[govee] Fix brightness vs. color synchronization (#17812)

* [govee] Fix synchronization of brightness

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2024-12-23 13:31:00 +00:00 committed by GitHub
parent 5c762848b5
commit 5eb47a042f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 863 additions and 463 deletions

View File

@ -20,68 +20,133 @@ While Govee provides probably more than a hundred different lights, only the fol
Here is a list of the supported devices (the ones marked with * have been tested by the author)
- H619Z RGBIC Pro LED Strip Lights
- H6042 Govee TV Light Bar #2
- H6043 Govee TV Light Bars #2
- H6046 RGBIC TV Light Bars
- H6047 RGBIC Gaming Light Bars with Smart Controller
- H6051 Aura - Smart Table Lamp
- H6052 Govee Table Lamp
- H6056 H6056 Flow Plus
- H6059 RGBWW Night Light for Kids
- H6061 Glide Hexa LED Panels (*)
- H6062 Glide Wall Light
- H6063 Gaming Wall Light
- H6065 Glide RGBIC Y Lights
- H6066 Glide Hexa Pro LED Panel
- H6067 Glide Triangle Light Panels (*)
- H606A Glide Hexa Light Panel Ultra
- H6072 RGBICWW Corner Floor Lamp (*)
- H6076 RGBICW Smart Corner Floor Lamp (*)
- H6073 LED Floor Lamp
- H6076 RGBICW Smart Corner Floor Lamp (*)
- H6078 Cylinder Floor Lamp
- H607C Floor Lamp #2
- H6087 RGBIC Smart Wall Sconces
- H6173 RGBIC Outdoor Strip Lights
- H619A RGBIC Strip Lights With Protective Coating 5M
- H619B RGBIC LED Strip Lights With Protective Coating
- H619C LED Strip Lights With Protective Coating
- H619D RGBIC PRO LED Strip Lights
- H619E RGBIC LED Strip Lights With Protective Coating
- H61A0 RGBIC Neon Rope Light 1M
- H61A1 RGBIC Neon Rope Light 2M
- H61A2 RGBIC Neon Rope Light 5M
- H61A3 RGBIC Neon Rope Light
- H61C5 RGBIC LED Neon Rope Lights for Desks (*)
- H61D3 Neon Rope Light 2 3M (*)
- H61D5 Neon Rope Light 2 5M (*)
- H61A5 Neon LED Strip Light 10
- H61A8Neon Neon Rope Light 10
- H618A RGBIC Basic LED Strip Lights 5M
- H618C RGBIC Basic LED Strip Lights 5M
- H6117 Dream Color LED Strip Light 10M
- H6159 RGB Light Strip (*)
- H615E LED Strip Lights 30M
- H6163 Dreamcolor LED Strip Light 5M
- H6088 RGBIC Cube Wall Sconces
- H608A String Downlights 5M
- H608B String Downlights 3M
- H608C String Downlights 2M
- H608D String Downlights 10M
- H60A0 Ceiling Light
- H60A1 Smart Ceiling Light (*)
- H610A Glide Lively Wall Lights
- H610B Music Wall Lights
- H6110 2x5M Multicolor with Alexa
- H6117 Dream Color LED Strip Light 10M
- H6141 5M Smart Multicolor Strip Light
- H6143 5M Strip Light
- H6144 2x5M Strip Light
- H6159 RGB Light Strip (*)
- H615A 5M Light Strip with Alexa (*)
- H615B 10M Light Strip with Alexa
- H615C 15M Light Strip with Alexa
- H615D 20M Light Strip with Alexa
- H615E 30M Light Strip with Alexa
- H6163 Dreamcolor LED Strip Light 5M
- H6167 TV Backlight 2.4M
- H6168 TV Backlight 2x0.7M+2x1.2M
- H616C Outdoor Strip 10M
- H616D Outdoor Strip 2x7.5M
- H616E Outdoor Strip 2x10M
- H6172 Outdoor LED Strip 10m
- H61B2 RGBIC Neon TV Backlight
- H6173 RGBIC Outdoor Strip Lights
- H6175 RGBIC Outdoor Strip Lights 10M
- H6176 RGBIC Outdoor Strip Lights 30M
- H6182 WiFi Multicolor TV Strip Light
- H618A RGBIC Basic LED Strip Lights 5M
- H618C RGBIC Basic LED Strip Lights 5M
- H618E LED Strip Lights 22m
- H618F RGBIC LED Strip Lights
- H619A Strip Lights With Protective Coating 5M
- H619B Strip Lights With Protective Coating 7.5M
- H619C Strip Lights With Protective Coating with Alexa 10M
- H619D PRO LED Strip Lights with Alexa 2x7.5M
- H619E Strip Lights With Protective Coating with Alexa 2x10M
- H619Z Pro LED Strip Lights 3M
- H61A0 RGBIC Neon Rope Light 3M
- H61A1 RGBIC Neon Rope Light 2M
- H61A2 RGBIC Neon Rope Light 5M
- H61A3 RGBIC Neon Rope Light 4M
- H61A5 Neon LED Strip Light 10M
- H61A8 Neon Rope Light 10M
- H61A8 Neon Rope Light 20M
- H61B1 Strip Light with Cover 5M
- H61B2 RGBIC Neon TV Backlight 3M
- H61BA LED Strip Light 5M
- H61BC LED Strip Light 10M
- H61BE LED Strip Light 2x10M
- H61C2 Neon LED Strip Light 2M
- H61C2 Neon LED Strip Light 3M
- H61C2 Neon LED Strip Light 5M
- H61D3 Neon Rope Light 2 3m (*)
- H61D5 Neon Rope Light 2 5m (*)
- H61E0 LED Strip Light M1
- H61E1 LED Strip Light M1
- H7012 Warm White Outdoor String Lights
- H7013 Warm White Outdoor String Lights
- H7021 RGBIC Warm White Smart Outdoor String
- H7028 Lynx Dream LED-Bulb String
- H7033 LED-Bulb String Lights
- H7041 LED Outdoor Bulb String Lights
- H7042 LED Outdoor Bulb String Lights
- H705A Permanent Outdoor Lights 30M
- H705B Permanent Outdoor Lights 15M
- H7050 Outdoor Ground Lights 11M
- H7051 Outdoor Ground Lights 15M
- H7052 Outdoor Ground Lights 15M
- H7052 Outdoor Ground Lights 30M
- H7055 Pathway Light
- H705A Permanent Outdoor Lights 30M
- H705B Permanent Outdoor Lights 15M
- H705C Permanent Outdoor Lights 45M
- H705D Permanent Outdoor Lights #2 15M
- H705E Permanent Outdoor Lights #2 30M
- H705F Permanent Outdoor Lights #2 45M
- H7060 LED Flood Lights (2-Pack)
- H7061 LED Flood Lights (4-Pack)
- H7062 LED Flood Lights (6-Pack)
- H7063 Outdoor Flood Lights
- H7065 Outdoor Spot Lights
- H70C1 Govee Christmas String Lights 10m (*)
- H70C2 Govee Christmas String Lights 20m (*)
- H6051 Aura - Smart Table Lamp
- H6056 H6056 Flow Plus
- H6059 RGBWW Night Light for Kids
- H618F RGBIC LED Strip Lights
- H618E LED Strip Lights 22m
- H6168 TV LED Backlight
- H7066 Outdoor Spot Lights
- H706A Permanent Outdoor Lights Pro 30M
- H706B Permanent Outdoor Lights Pro 45M
- H706C Permanent Outdoor Lights Pro 60M
- H7070 Outdoor Projector Light (*)
- H7075 Outdoor Wall Light
- H70B1 520 LED Curtain Lights
- H70BC 400 LED Curtain Lights
- H70C1 RGBIC String Light 10M (*)
- H70C2 RGBIC String Light 20M (*)
- H805A Permanent Outdoor Lights Elite 30M
- H805B Permanent Outdoor Lights Elite 15M
- H805C Permanent Outdoor Lights Elite 45M
## Firewall
Govee devices communicate via multicast and unicast messages over the LAN.
So you must ensure that any firewall on your openHAB server is configured to pass the following traffic:
- Multicast UDP on 239.255.255.250 port 4001
- Incoming unicast UDP on port 4002
- Outgoing unicast UDP on port 4003
## Discovery
Discovery is done by scanning the devices in the Thing section.
@ -108,11 +173,13 @@ arp -a | grep "MAC_ADDRESS"
### `govee-light` Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
| macAddress | text | MAC address of the device | N/A | yes | no |
| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|------------------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
| macAddress | text | MAC address of the device | N/A | yes | no |
| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
| minKelvin | integer | The minimum color temperature that the light supports in Kelvin. | N/A | no | yes |
| maxKelvin | integer | The maximum color temperature that the light supports in Kelvin. | N/A | no | yes |
## Channels

View File

@ -15,13 +15,23 @@ package org.openhab.binding.govee.internal;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.DatagramChannel;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -29,6 +39,7 @@ import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -36,231 +47,259 @@ import com.google.gson.Gson;
import com.google.gson.JsonParseException;
/**
* The {@link CommunicationManager} is a thread that handles the answers of all devices.
* Therefore it needs to apply the information to the right thing.
*
* Discovery uses the same response code, so we must not refresh the status during discovery.
* The {@link CommunicationManager} component implements a sender to send commands to Govee devices,
* and implements a thread that handles the notifications from all devices. It applies the status
* information to the right Thing. It supports both discovery and status commands and notifications
* concurrently.
*
* @author Stefan Höhn - Initial contribution
* @author Danny Baumann - Thread-Safe design refactoring
* @author Andrew Fiddian-Green - New threading model using java.nio channel
*/
@NonNullByDefault
@Component(service = CommunicationManager.class)
public class CommunicationManager {
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
private final Gson gson = new Gson();
// Holds a list of all thing handlers to send them thing updates via the receiver-Thread
private final Map<String, GoveeHandler> thingHandlers = new HashMap<>();
@Nullable
private StatusReceiver receiverThread;
// list of Thing handler listeners that will receive state notifications
private final Map<String, GoveeHandler> thingHandlerListeners = new ConcurrentHashMap<>();
private @Nullable GoveeDiscoveryListener discoveryListener;
private @Nullable Thread serverThread;
private boolean serverStopFlag = false;
private final Object paramsLock = new Object();
private final Object serverLock = new Object();
private final Object senderLock = new Object();
private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250";
private static final int DISCOVERY_PORT = 4001;
private static final int RESPONSE_PORT = 4002;
private static final int REQUEST_PORT = 4003;
private static final int INTERFACE_TIMEOUT_SEC = 5;
public static final int SCAN_TIMEOUT_SEC = 5;
private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";
public interface DiscoveryResultReceiver {
void onResultReceived(DiscoveryResponse result);
private static final InetSocketAddress DISCOVERY_SOCKET_ADDRESS = new InetSocketAddress(DISCOVERY_MULTICAST_ADDRESS,
DISCOVERY_PORT);
public interface GoveeDiscoveryListener {
void onDiscoveryResponse(DiscoveryResponse discoveryResponse);
}
@Activate
public CommunicationManager() {
serverStart();
}
@Deactivate
public void deactivate() {
thingHandlerListeners.clear();
discoveryListener = null;
serverStop();
}
/**
* Thing handlers register themselves to receive state updates when they are initialized.
*/
public void registerHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.put(handler.getHostname(), handler);
if (receiverThread == null) {
receiverThread = new StatusReceiver();
receiverThread.start();
}
}
thingHandlerListeners.put(ipAddressFrom(handler.getHostname()), handler);
}
/**
* Thing handlers unregister themselves when they are destroyed.
*/
public void unregisterHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.remove(handler.getHostname());
if (thingHandlers.isEmpty()) {
StatusReceiver receiver = receiverThread;
if (receiver != null) {
receiver.stopReceiving();
}
receiverThread = null;
}
}
thingHandlerListeners.remove(ipAddressFrom(handler.getHostname()));
}
/**
* Send a unicast command request to the device.
*/
public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException {
final String hostname = handler.getHostname();
final DatagramSocket socket = new DatagramSocket();
socket.setReuseAddress(true);
final String message = gson.toJson(request);
final byte[] data = message.getBytes();
final InetAddress address = InetAddress.getByName(hostname);
DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
logger.trace("Sending {} to {}", message, hostname);
socket.send(packet);
socket.close();
}
public void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultReceiver receiver) throws IOException {
synchronized (receiver) {
StatusReceiver localReceiver = null;
StatusReceiver activeReceiver = null;
try {
if (receiverThread == null) {
localReceiver = new StatusReceiver();
localReceiver.start();
activeReceiver = localReceiver;
} else {
activeReceiver = receiverThread;
}
if (activeReceiver != null) {
activeReceiver.setDiscoveryResultsReceiver(receiver);
}
final InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS);
final InetSocketAddress socketAddress = new InetSocketAddress(broadcastAddress, RESPONSE_PORT);
final Instant discoveryStartTime = Instant.now();
final Instant discoveryEndTime = discoveryStartTime.plusSeconds(INTERFACE_TIMEOUT_SEC);
try (MulticastSocket sendSocket = new MulticastSocket(socketAddress)) {
sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000);
sendSocket.setReuseAddress(true);
sendSocket.setBroadcast(true);
sendSocket.setTimeToLive(2);
sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, RESPONSE_PORT), intf);
byte[] requestData = DISCOVER_REQUEST.getBytes();
DatagramPacket request = new DatagramPacket(requestData, requestData.length, broadcastAddress,
DISCOVERY_PORT);
sendSocket.send(request);
}
do {
try {
receiver.wait(INTERFACE_TIMEOUT_SEC * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (Instant.now().isBefore(discoveryEndTime));
} finally {
if (activeReceiver != null) {
activeReceiver.setDiscoveryResultsReceiver(null);
}
if (localReceiver != null) {
localReceiver.stopReceiving();
}
serverStart();
synchronized (senderLock) {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setReuseAddress(true);
String message = gson.toJson(request);
byte[] data = message.getBytes();
String hostname = handler.getHostname();
InetAddress address = InetAddress.getByName(hostname);
DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
socket.send(packet);
logger.trace("Sent request to {} on {} with content = {}", handler.getThing().getUID(),
address.getHostAddress(), message);
}
}
}
private class StatusReceiver extends Thread {
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
private boolean stopped = false;
private @Nullable DiscoveryResultReceiver discoveryResultReceiver;
/**
* Send discovery multicast pings on any ipv4 address bound to any network interface in the given
* list and then sleep for sufficient time until responses may have been received.
*/
public void runDiscoveryForInterfaces(List<NetworkInterface> interfaces, GoveeDiscoveryListener listener) {
serverStart();
try {
discoveryListener = listener;
Instant sleepUntil = Instant.now().plusSeconds(SCAN_TIMEOUT_SEC);
private @Nullable MulticastSocket socket;
interfaces.parallelStream() // send on all interfaces in parallel
.forEach(interFace -> Collections.list(interFace.getInetAddresses()).stream()
.filter(address -> address instanceof Inet4Address).map(address -> address.getHostAddress())
.forEach(ipv4Address -> sendPing(interFace, ipv4Address)));
StatusReceiver() {
super("GoveeStatusReceiver");
}
synchronized void setDiscoveryResultsReceiver(@Nullable DiscoveryResultReceiver receiver) {
discoveryResultReceiver = receiver;
}
void stopReceiving() {
stopped = true;
interrupt();
if (socket != null) {
socket.close();
}
try {
join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void run() {
while (!stopped) {
Duration sleepDuration = Duration.between(Instant.now(), sleepUntil);
if (!sleepDuration.isNegative()) {
try {
socket = new MulticastSocket(RESPONSE_PORT);
byte[] buffer = new byte[10240];
socket.setReuseAddress(true);
while (!stopped) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
if (!socket.isClosed()) {
socket.receive(packet);
} else {
logger.warn("Socket was unexpectedly closed");
break;
}
if (stopped) {
break;
}
String response = new String(packet.getData(), packet.getOffset(), packet.getLength());
String deviceIPAddress = packet.getAddress().toString().replace("/", "");
logger.trace("Response from {} = {}", deviceIPAddress, response);
final DiscoveryResultReceiver discoveryReceiver;
synchronized (this) {
discoveryReceiver = discoveryResultReceiver;
}
if (discoveryReceiver != null) {
// We're in discovery mode: try to parse result as discovery message and signal the receiver
// if parsing was successful
try {
DiscoveryResponse result = gson.fromJson(response, DiscoveryResponse.class);
if (result != null) {
synchronized (discoveryReceiver) {
discoveryReceiver.onResultReceived(result);
discoveryReceiver.notifyAll();
}
}
} catch (JsonParseException e) {
logger.debug(
"JsonParseException when trying to parse the response, probably a status message",
e);
}
} else {
final @Nullable GoveeHandler handler;
synchronized (thingHandlers) {
handler = thingHandlers.get(deviceIPAddress);
}
if (handler == null) {
logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress);
} else {
logger.debug("processing status updates for thing {} ", handler.getThing().getLabel());
handler.handleIncomingStatus(response);
}
}
}
} catch (IOException e) {
logger.warn("exception when receiving status packet", e);
// as we haven't received a packet we also don't know where it should have come from
// hence, we don't know which thing put offline.
// a way to monitor this would be to keep track in a list, which device answers we expect
// and supervise an expected answer within a given time but that will make the whole
// mechanism much more complicated and may be added in the future
} finally {
if (socket != null) {
socket.close();
socket = null;
}
Thread.sleep(sleepDuration.toMillis());
} catch (InterruptedException e) {
// just return
}
}
} finally {
discoveryListener = null;
}
}
/**
* This method gets executed on the server thread. It uses a {@link DatagramChannel} to listen on port
* 4002 and it processes any notifications received. The task runs continuously in a loop until the
* thread is externally interrupted.
*
* <li>In case of status notifications it forwards the message to the Thing handler listener.</li>
* <li>In case of discovery notifications it forwards the message to the discovery listener.</li>
* <li>If there is neither a Thing handler listener, nor a discovery listener, it logs an error.</li>
*/
private void serverThreadTask() {
synchronized (serverLock) {
try {
logger.trace("Server thread started.");
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (!serverStopFlag) {
try (DatagramChannel channel = DatagramChannel.open()
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
.bind(new InetSocketAddress(RESPONSE_PORT))) {
while (!serverStopFlag) {
String sourceIp = "";
try {
SocketAddress socketAddress = channel.receive(buffer.clear());
if ((socketAddress instanceof InetSocketAddress inetSocketAddress)
&& (inetSocketAddress.getAddress() instanceof InetAddress inetAddress)) {
sourceIp = inetAddress.getHostAddress();
} else {
logger.debug("Receive() - bad socketAddress={}", socketAddress);
return;
}
} catch (ClosedByInterruptException e) {
// thrown if 'Thread.interrupt()' is called during 'channel.receive()'
logger.debug("Receive ClosedByInterruptException, isInterrupted={}, serverStopFlag={}",
Thread.currentThread().isInterrupted(), serverStopFlag);
Thread.interrupted(); // clear 'interrupted' flag
break;
} catch (IOException e) {
logger.debug("Receive unexpected exception={}", e.getMessage());
break;
}
String message = new String(buffer.array(), 0, buffer.position());
logger.trace("Receive from sourceIp={}, message={}", sourceIp, message);
GoveeHandler handler = thingHandlerListeners.get(sourceIp);
boolean devStatus = message.contains("devStatus");
if (handler != null && devStatus) {
logger.debug("Notifying status of thing={} on sourcecIp={}",
handler.getThing().getUID(), sourceIp);
handler.handleIncomingStatus(message);
continue;
}
GoveeDiscoveryListener discoveryListener = this.discoveryListener;
if (!devStatus && discoveryListener != null) {
try {
DiscoveryResponse response = gson.fromJson(message, DiscoveryResponse.class);
if (response != null) {
logger.debug("Notifying discovery of device on sourceIp={}", sourceIp);
discoveryListener.onDiscoveryResponse(response);
}
} catch (JsonParseException e) {
logger.debug("Discovery notification parse exception={}", e.getMessage());
}
continue;
}
logger.warn(
"Unhandled message with sourceIp={}, devStatus={}, handler={}, discoveryListener={}",
sourceIp, devStatus, handler, discoveryListener);
} // end of inner while loop
} catch (IOException e) {
logger.debug("Datagram channel create exception={}", e.getMessage());
}
} // end of outer while loop
} finally {
serverThread = null;
serverStopFlag = false;
logger.trace("Server thread terminated.");
}
}
}
/**
* Get the resolved IP address from the given host name.
*/
private static String ipAddressFrom(String host) {
try {
return InetAddress.getByName(host).getHostAddress();
} catch (UnknownHostException e) {
}
return host;
}
/**
* Starts the server thread if it is not already running.
*/
private void serverStart() {
synchronized (paramsLock) {
Thread serverthread = serverThread;
if (serverthread == null) {
serverthread = new Thread(this::serverThreadTask, "OH-binding-" + GoveeBindingConstants.BINDING_ID);
serverThread = serverthread;
serverStopFlag = false;
serverthread.start();
}
}
}
/**
* Stops the server thread.
*/
private void serverStop() {
synchronized (paramsLock) {
serverStopFlag = true;
Thread serverthread = serverThread;
if (serverthread != null) {
serverthread.interrupt();
}
}
}
/**
* Send discovery ping multicast on the given network interface and ipv4 address.
*/
private void sendPing(NetworkInterface interFace, String ipv4Address) {
try (DatagramChannel channel = (DatagramChannel) DatagramChannel.open(StandardProtocolFamily.INET)
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
.setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64)
.setOption(StandardSocketOptions.IP_MULTICAST_IF, interFace)
.bind(new InetSocketAddress(ipv4Address, DISCOVERY_PORT)).configureBlocking(false)) {
channel.send(ByteBuffer.wrap(DISCOVER_REQUEST.getBytes()), DISCOVERY_SOCKET_ADDRESS);
logger.trace("Sent ping from {}:{} ({}) to {}:{} with content = {}", ipv4Address, DISCOVERY_PORT,
interFace.getDisplayName(), DISCOVERY_MULTICAST_ADDRESS, DISCOVERY_PORT, DISCOVER_REQUEST);
} catch (IOException e) {
logger.debug("Network error", e);
}
}
}

View File

@ -31,7 +31,7 @@ public class GoveeBindingConstants {
public static final String PRODUCT_NAME = "productName";
public static final String HW_VERSION = "wifiHardwareVersion";
public static final String SW_VERSION = "wifiSoftwareVersion";
private static final String BINDING_ID = "govee";
public static final String BINDING_ID = "govee";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light");
@ -44,4 +44,7 @@ public class GoveeBindingConstants {
// Limit values of channels
public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0;
public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.0;
public static final String PROPERTY_COLOR_TEMPERATURE_MIN = "minKelvin";
public static final String PROPERTY_COLOR_TEMPERATURE_MAX = "maxKelvin";
}

View File

@ -13,6 +13,7 @@
package org.openhab.binding.govee.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler
@ -24,4 +25,7 @@ public class GoveeConfiguration {
public String hostname = "";
public int refreshInterval = 5; // in seconds
public @Nullable Integer minKelvin;
public @Nullable Integer maxKelvin;
}

View File

@ -12,16 +12,18 @@
*/
package org.openhab.binding.govee.internal;
import java.io.IOException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.govee.internal.CommunicationManager.GoveeDiscoveryListener;
import org.openhab.binding.govee.internal.model.DiscoveryData;
import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
@ -79,17 +81,22 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee")
public class GoveeDiscoveryService extends AbstractDiscoveryService {
public class GoveeDiscoveryService extends AbstractDiscoveryService implements GoveeDiscoveryListener {
private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class);
private CommunicationManager communicationManager;
private final CommunicationManager communicationManager;
private @Nullable ScheduledFuture<?> backgroundScanTask;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
@Activate
public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider,
@Reference CommunicationManager communicationManager) {
super(SUPPORTED_THING_TYPES_UIDS, 0, false);
public GoveeDiscoveryService(final @Reference TranslationProvider i18nProvider,
final @Reference LocaleProvider localeProvider,
final @Reference CommunicationManager communicationManager) {
super(SUPPORTED_THING_TYPES_UIDS, CommunicationManager.SCAN_TIMEOUT_SEC, true);
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
this.communicationManager = communicationManager;
@ -103,23 +110,8 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
@Override
protected void startScan() {
logger.debug("starting Scan");
getLocalNetworkInterfaces().forEach(localNetworkInterface -> {
logger.debug("Discovering Govee devices on {} ...", localNetworkInterface);
try {
communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> {
DiscoveryResult result = responseToResult(response);
if (result != null) {
thingDiscovered(result);
}
});
logger.trace("After runDiscoveryForInterface");
} catch (IOException e) {
logger.debug("Discovery with IO exception: {}", e.getMessage());
}
logger.trace("After try");
});
logger.debug("Starting scan");
scheduler.schedule(this::doDiscovery, 0, TimeUnit.MILLISECONDS);
}
public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
@ -165,11 +157,11 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
}
String hwVersion = data.wifiVersionHard();
if (hwVersion != null) {
if (!hwVersion.isEmpty()) {
builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
}
String swVersion = data.wifiVersionSoft();
if (swVersion != null) {
if (!swVersion.isEmpty()) {
builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
}
@ -194,4 +186,41 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
}
return result;
}
/**
* Command the {@link CommunicationManager) to run the scans.
*/
private void doDiscovery() {
communicationManager.runDiscoveryForInterfaces(getLocalNetworkInterfaces(), this);
}
/**
* This method is called back by the {@link CommunicationManager} when it receives a {@link DiscoveryResponse}
* notification carrying information about potential newly discovered Things.
*/
@Override
public synchronized void onDiscoveryResponse(DiscoveryResponse discoveryResponse) {
DiscoveryResult discoveryResult = responseToResult(discoveryResponse);
if (discoveryResult != null) {
thingDiscovered(discoveryResult);
}
}
@Override
protected void startBackgroundDiscovery() {
ScheduledFuture<?> backgroundScanTask = this.backgroundScanTask;
if (backgroundScanTask == null || backgroundScanTask.isCancelled()) {
this.backgroundScanTask = scheduler.scheduleWithFixedDelay(this::doDiscovery, 0,
BACKGROUND_SCAN_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
ScheduledFuture<?> backgroundScanTask = this.backgroundScanTask;
if (backgroundScanTask != null) {
backgroundScanTask.cancel(true);
this.backgroundScanTask = null;
}
}
}

View File

@ -15,6 +15,11 @@ package org.openhab.binding.govee.internal;
import static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -24,10 +29,12 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.govee.internal.model.Color;
import org.openhab.binding.govee.internal.model.ColorData;
import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData;
import org.openhab.binding.govee.internal.model.GenericGoveeData;
import org.openhab.binding.govee.internal.model.GenericGoveeMsg;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.openhab.binding.govee.internal.model.StatusResponse;
import org.openhab.binding.govee.internal.model.ValueIntData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
@ -40,6 +47,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -71,47 +79,73 @@ import com.google.gson.JsonSyntaxException;
* https://app-h5.govee.com/user-manual/wlan-guide
*
* @author Stefan Höhn - Initial contribution
* @author Andrew Fiddian-Green - Added sequential task processing
*/
@NonNullByDefault
public class GoveeHandler extends BaseThingHandler {
/*
* Messages to be sent to the Govee devices
*/
private static final Gson GSON = new Gson();
private static final int REFRESH_SECONDS_MIN = 2;
private static final int INTER_COMMAND_DELAY_MILLISEC = 100;
private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
protected ScheduledExecutorService executorService = scheduler;
@Nullable
private ScheduledFuture<?> triggerStatusJob; // send device status update job
private @Nullable ScheduledFuture<?> thingTaskSenderTask;
private GoveeConfiguration goveeConfiguration = new GoveeConfiguration();
private CommunicationManager communicationManager;
private final CommunicationManager communicationManager;
private final GoveeStateDescriptionProvider stateDescriptionProvider;
private final List<Callable<Boolean>> taskQueue = new ArrayList<>();
private int lastOnOff;
private int lastBrightness;
private OnOffType lastSwitch = OnOffType.OFF;
private HSBType lastColor = new HSBType();
private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue();
private int lastKelvin;
private int minKelvin;
private int maxKelvin;
private int refreshIntervalSeconds;
private Instant nextRefreshDueTime = Instant.EPOCH;
/**
* This thing related job <i>thingRefreshSender</i> triggers an update to the Govee device.
* The device sends it back to the common port and the response is
* then received by the common #refreshStatusReceiver
* This thing related job <i>thingTaskSender</i> sends the next queued command (if any)
* to the Govee device. If there is no queued command and a regular refresh is due then
* sends the command to trigger a status refresh.
*
* The device may send a reply to the common port and if so the response is received by
* the refresh status receiver.
*/
private final Runnable thingRefreshSender = () -> {
try {
triggerDeviceStatusRefresh();
updateStatus(ThingStatus.ONLINE);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ "\"]");
private final Runnable thingTaskSender = () -> {
synchronized (taskQueue) {
if (taskQueue.isEmpty() && Instant.now().isBefore(nextRefreshDueTime)) {
return; // no queued command nor pending refresh
}
if (taskQueue.isEmpty()) {
taskQueue.add(() -> triggerDeviceStatusRefresh());
nextRefreshDueTime = Instant.now().plusSeconds(refreshIntervalSeconds);
} else if (taskQueue.size() > 20) {
logger.info("Command task queue size:{} exceeds limit:20", taskQueue.size());
}
try {
if (taskQueue.remove(0).call()) {
updateStatus(ThingStatus.ONLINE);
}
} catch (IndexOutOfBoundsException e) {
logger.warn("Unexpected List.remove() exception:{}", e.getMessage());
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ "\"]");
}
}
};
public GoveeHandler(Thing thing, CommunicationManager communicationManager) {
public GoveeHandler(Thing thing, CommunicationManager communicationManager,
GoveeStateDescriptionProvider stateDescriptionProvider) {
super(thing);
this.communicationManager = communicationManager;
this.stateDescriptionProvider = stateDescriptionProvider;
}
public String getHostname() {
@ -128,140 +162,176 @@ public class GoveeHandler extends BaseThingHandler {
"@text/offline.configuration-error.ip-address.missing");
return;
}
minKelvin = Objects.requireNonNullElse(goveeConfiguration.minKelvin, COLOR_TEMPERATURE_MIN_VALUE.intValue());
maxKelvin = Objects.requireNonNullElse(goveeConfiguration.maxKelvin, COLOR_TEMPERATURE_MAX_VALUE.intValue());
if ((minKelvin < COLOR_TEMPERATURE_MIN_VALUE) || (maxKelvin > COLOR_TEMPERATURE_MAX_VALUE)
|| (minKelvin >= maxKelvin)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error.invalid-color-temperature-range");
return;
}
thing.setProperty(PROPERTY_COLOR_TEMPERATURE_MIN, Integer.toString(minKelvin));
thing.setProperty(PROPERTY_COLOR_TEMPERATURE_MAX, Integer.toString(maxKelvin));
stateDescriptionProvider.setMinMaxKelvin(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE_ABS),
minKelvin, maxKelvin);
refreshIntervalSeconds = goveeConfiguration.refreshInterval;
if (refreshIntervalSeconds < REFRESH_SECONDS_MIN) {
logger.warn("Config Param refreshInterval={} too low, minimum={}", refreshIntervalSeconds,
REFRESH_SECONDS_MIN);
refreshIntervalSeconds = REFRESH_SECONDS_MIN;
}
updateStatus(ThingStatus.UNKNOWN);
communicationManager.registerHandler(this);
if (triggerStatusJob == null) {
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
triggerStatusJob = executorService.scheduleWithFixedDelay(thingRefreshSender, 100,
goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS);
if (thingTaskSenderTask == null) {
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
thingTaskSenderTask = executorService.scheduleWithFixedDelay(thingTaskSender, INTER_COMMAND_DELAY_MILLISEC,
INTER_COMMAND_DELAY_MILLISEC, TimeUnit.MILLISECONDS);
}
}
@Override
public void dispose() {
super.dispose();
ScheduledFuture<?> triggerStatusJobFuture = triggerStatusJob;
if (triggerStatusJobFuture != null) {
triggerStatusJobFuture.cancel(true);
triggerStatusJob = null;
taskQueue.clear();
ScheduledFuture<?> job = thingTaskSenderTask;
if (job != null) {
job.cancel(true);
thingTaskSenderTask = null;
}
communicationManager.unregisterHandler(this);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
try {
public void handleCommand(ChannelUID channelUID, Command commandParam) {
Command command = commandParam;
synchronized (taskQueue) {
logger.debug("handleCommand({}, {})", channelUID, command);
if (command instanceof RefreshType) {
// we are refreshing all channels at once, as we get all information at the same time
triggerDeviceStatusRefresh();
logger.debug("Triggering Refresh");
taskQueue.add(() -> triggerDeviceStatusRefresh());
} else {
logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass());
switch (channelUID.getId()) {
case CHANNEL_COLOR:
if (command instanceof HSBType hsbCommand) {
int[] rgb = ColorUtil.hsbToRgb(hsbCommand);
sendColor(new Color(rgb[0], rgb[1], rgb[2]));
} else if (command instanceof PercentType percent) {
sendBrightness(percent.intValue());
} else if (command instanceof OnOffType onOffCommand) {
sendOnOff(onOffCommand);
if (command instanceof HSBType hsb) {
taskQueue.add(() -> sendColor(hsb));
command = hsb.getBrightness(); // fall through
}
if (command instanceof PercentType percent) {
taskQueue.add(() -> sendBrightness(percent));
command = OnOffType.from(percent.intValue() > 0); // fall through
}
if (command instanceof OnOffType onOff) {
taskQueue.add(() -> sendOnOff(onOff));
taskQueue.add(() -> triggerDeviceStatusRefresh());
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof PercentType percent) {
logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", command);
Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.intValue()
* (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0);
lastColorTempInKelvin = colorTemp.intValue();
logger.debug("lastColorTempInKelvin {}", lastColorTempInKelvin);
sendColorTemp(lastColorTempInKelvin);
taskQueue.add(() -> sendKelvin(percentToKelvin(percent)));
taskQueue.add(() -> triggerDeviceStatusRefresh());
}
break;
case CHANNEL_COLOR_TEMPERATURE_ABS:
if (command instanceof QuantityType<?> quantity) {
logger.debug("Color Temperature Absolute change with Percent Type {}", command);
lastColorTempInKelvin = quantity.intValue();
logger.debug("COLOR_TEMPERATURE_ABS: lastColorTempInKelvin {}", lastColorTempInKelvin);
int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin
- COLOR_TEMPERATURE_MIN_VALUE)
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
logger.debug("computed lastColorTempInPercent {}", lastColorTempInPercent);
sendColorTemp(lastColorTempInKelvin);
if (command instanceof QuantityType<?> genericQuantity) {
QuantityType<?> kelvin = genericQuantity.toInvertibleUnit(Units.KELVIN);
if (kelvin == null) {
logger.warn("handleCommand() invalid QuantityType:{}", genericQuantity);
break;
}
taskQueue.add(() -> sendKelvin(kelvin.intValue()));
taskQueue.add(() -> triggerDeviceStatusRefresh());
} else if (command instanceof DecimalType kelvin) {
taskQueue.add(() -> sendKelvin(kelvin.intValue()));
taskQueue.add(() -> triggerDeviceStatusRefresh());
}
break;
}
}
updateStatus(ThingStatus.ONLINE);
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ "\"]");
}
}
/**
* Initiate a refresh to our thing devicee
*
* Initiate a refresh to our thing device
*/
private void triggerDeviceStatusRefresh() throws IOException {
logger.debug("trigger Refresh Status of device {}", thing.getLabel());
GenericGoveeRequest lightQuery = new GenericGoveeRequest(
new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData()));
communicationManager.sendRequest(this, lightQuery);
}
public void sendColor(Color color) throws IOException {
lastColor = ColorUtil.rgbToHsb(new int[] { color.r(), color.g(), color.b() });
GenericGoveeRequest lightColor = new GenericGoveeRequest(
new GenericGoveeMsg("colorwc", new ColorData(color, 0)));
communicationManager.sendRequest(this, lightColor);
}
public void sendBrightness(int brightness) throws IOException {
lastBrightness = brightness;
GenericGoveeRequest lightBrightness = new GenericGoveeRequest(
new GenericGoveeMsg("brightness", new ValueIntData(brightness)));
communicationManager.sendRequest(this, lightBrightness);
}
private void sendOnOff(OnOffType switchValue) throws IOException {
lastOnOff = (switchValue == OnOffType.ON) ? 1 : 0;
GenericGoveeRequest switchLight = new GenericGoveeRequest(
new GenericGoveeMsg("turn", new ValueIntData(lastOnOff)));
communicationManager.sendRequest(this, switchLight);
}
private void sendColorTemp(int colorTemp) throws IOException {
lastColorTempInKelvin = colorTemp;
logger.debug("sendColorTemp {}", colorTemp);
GenericGoveeRequest lightColor = new GenericGoveeRequest(
new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp)));
communicationManager.sendRequest(this, lightColor);
private boolean triggerDeviceStatusRefresh() throws IOException {
logger.debug("triggerDeviceStatusRefresh() to {}", thing.getUID());
GenericGoveeData data = new EmptyValueQueryStatusData();
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("devStatus", data));
communicationManager.sendRequest(this, request);
return true;
}
/**
* Creates a Color state by using the last color information from lastColor
* The brightness is overwritten either by the provided lastBrightness
* or if lastOnOff = 0 (off) then the brightness is set 0
*
* @see #lastColor
* @see #lastBrightness
* @see #lastOnOff
*
* @return the computed state
* Send the normalized RGB color parameters.
*/
private HSBType getColorState(Color color, int brightness) {
PercentType computedBrightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(brightness);
int[] rgb = { color.r(), color.g(), color.b() };
HSBType hsb = ColorUtil.rgbToHsb(rgb);
return new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness);
public boolean sendColor(HSBType color) throws IOException {
logger.debug("sendColor({}) to {}", color, thing.getUID());
int[] normalRGB = ColorUtil.hsbToRgb(new HSBType(color.getHue(), color.getSaturation(), PercentType.HUNDRED));
GenericGoveeData data = new ColorData(new Color(normalRGB[0], normalRGB[1], normalRGB[2]), 0);
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", data));
communicationManager.sendRequest(this, request);
return true;
}
void handleIncomingStatus(String response) {
/**
* Send the brightness parameter.
*/
public boolean sendBrightness(PercentType brightness) throws IOException {
logger.debug("sendBrightness({}) to {}", brightness, thing.getUID());
GenericGoveeData data = new ValueIntData(brightness.intValue());
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("brightness", data));
communicationManager.sendRequest(this, request);
return true;
}
/**
* Send the on-off parameter.
*/
private boolean sendOnOff(OnOffType onOff) throws IOException {
logger.debug("sendOnOff({}) to {}", onOff, thing.getUID());
GenericGoveeData data = new ValueIntData(onOff == OnOffType.ON ? 1 : 0);
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("turn", data));
communicationManager.sendRequest(this, request);
return true;
}
/**
* Set the color temperature (Kelvin) parameter.
*/
private boolean sendKelvin(int kelvin) throws IOException {
logger.debug("sendKelvin({}) to {}", kelvin, thing.getUID());
GenericGoveeData data = new ColorData(new Color(0, 0, 0), kelvin);
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", data));
communicationManager.sendRequest(this, request);
return true;
}
/**
* Build an {@link HSBType} from the given normalized {@link Color} RGB parameters, brightness, and on-off state
* parameters. If the on parameter is true then use the brightness parameter, otherwise use a brightness of zero.
*
* @param normalRgbParams record containing the lamp's normalized RGB parameters (0..255)
* @param brightnessParam the lamp brightness in range 0..100
* @param onParam the lamp on-off state
*
* @return the respective HSBType
*/
private static HSBType buildHSB(Color normalRgbParams, int brightnessParam, boolean onParam) {
HSBType normalColor = ColorUtil
.rgbToHsb(new int[] { normalRgbParams.r(), normalRgbParams.g(), normalRgbParams.b() });
PercentType brightness = onParam ? new PercentType(brightnessParam) : PercentType.ZERO;
return new HSBType(normalColor.getHue(), normalColor.getSaturation(), brightness);
}
public void handleIncomingStatus(String response) {
if (response.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.empty-response");
@ -284,47 +354,52 @@ public class GoveeHandler extends BaseThingHandler {
return;
}
logger.trace("Receiving Device State");
int newOnOff = message.msg().data().onOff();
logger.trace("newOnOff = {}", newOnOff);
int newBrightness = message.msg().data().brightness();
logger.trace("newBrightness = {}", newBrightness);
Color newColor = message.msg().data().color();
logger.trace("newColor = {}", newColor);
int newColorTempInKelvin = message.msg().data().colorTemInKelvin();
logger.trace("newColorTempInKelvin = {}", newColorTempInKelvin);
logger.debug("updateDeviceState() for {}", thing.getUID());
newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE)
? COLOR_TEMPERATURE_MIN_VALUE.intValue()
: newColorTempInKelvin;
newColorTempInKelvin = (newColorTempInKelvin > COLOR_TEMPERATURE_MAX_VALUE)
? COLOR_TEMPERATURE_MAX_VALUE.intValue()
: newColorTempInKelvin;
OnOffType sw = OnOffType.from(message.msg().data().onOff() == 1);
int brightness = message.msg().data().brightness();
Color normalRGB = message.msg().data().color();
int kelvin = message.msg().data().colorTemInKelvin();
int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE)
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
logger.trace("Update values: switch:{}, brightness:{}, normalRGB:{}, kelvin:{}", sw, brightness, normalRGB,
kelvin);
HSBType adaptedColor = getColorState(newColor, newBrightness);
HSBType color = buildHSB(normalRGB, brightness, true);
logger.trace("HSB old: {} vs adaptedColor: {}", lastColor, adaptedColor);
// avoid noise by only updating if the value has changed on the device
if (!adaptedColor.equals(lastColor)) {
logger.trace("UPDATING HSB old: {} != {}", lastColor, adaptedColor);
updateState(CHANNEL_COLOR, adaptedColor);
logger.trace("Compare hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
if ((sw != lastSwitch) || !color.equals(lastColor)) {
logger.trace("Update hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
updateState(CHANNEL_COLOR, buildHSB(normalRGB, brightness, sw == OnOffType.ON));
lastSwitch = sw;
lastColor = color;
}
// avoid noise by only updating if the value has changed on the device
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
newColorTempInPercent, newColorTempInKelvin);
if (newColorTempInKelvin != lastColorTempInKelvin) {
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
newColorTempInPercent, newColorTempInKelvin);
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType<>(lastColorTempInKelvin, Units.KELVIN));
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(newColorTempInPercent));
logger.trace("Compare kelvin old:{} to new:{}", lastKelvin, kelvin);
if (kelvin != lastKelvin) {
logger.trace("Update kelvin old:{} to new:{}", lastKelvin, kelvin);
if (kelvin != 0) {
kelvin = Math.round(Math.min(maxKelvin, Math.max(minKelvin, kelvin)));
updateState(CHANNEL_COLOR_TEMPERATURE, kelvinToPercent(kelvin));
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, QuantityType.valueOf(kelvin, Units.KELVIN));
} else {
updateState(CHANNEL_COLOR_TEMPERATURE, UnDefType.UNDEF);
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, UnDefType.UNDEF);
}
lastKelvin = kelvin;
}
}
lastOnOff = newOnOff;
lastColor = adaptedColor;
lastBrightness = newBrightness;
/**
* Convert PercentType to Kelvin.
*/
private int percentToKelvin(PercentType percent) {
return (int) Math.round((((maxKelvin - minKelvin) * percent.doubleValue() / 100.0) + minKelvin));
}
/**
* Convert Kelvin to PercentType.
*/
private PercentType kelvinToPercent(int kelvin) {
return new PercentType((int) Math.round((kelvin - minKelvin) * 100.0 / (maxKelvin - minKelvin)));
}
}

View File

@ -38,11 +38,14 @@ import org.osgi.service.component.annotations.Reference;
public class GoveeHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT);
private CommunicationManager communicationManager;
private final CommunicationManager communicationManager;
private final GoveeStateDescriptionProvider stateDescriptionProvider;
@Activate
public GoveeHandlerFactory(@Reference CommunicationManager communicationManager) {
public GoveeHandlerFactory(final @Reference CommunicationManager communicationManager,
final @Reference GoveeStateDescriptionProvider stateDescriptionProvider) {
this.communicationManager = communicationManager;
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
@ -55,7 +58,7 @@ public class GoveeHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_LIGHT.equals(thingTypeUID)) {
return new GoveeHandler(thing, communicationManager);
return new GoveeHandler(thing, communicationManager, stateDescriptionProvider);
}
return null;

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.govee.internal;
import java.math.BigDecimal;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.events.ThingEventFactory;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link GoveeStateDescriptionProvider} provides state descriptions for different color temperature ranges.
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, GoveeStateDescriptionProvider.class })
public class GoveeStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>();
@Activate
public GoveeStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
StateDescriptionFragment stateDescriptionFragment = stateDescriptionFragments.get(channel.getUID());
return stateDescriptionFragment != null ? stateDescriptionFragment.toStateDescription()
: super.getStateDescription(channel, original, locale);
}
/**
* Set the state description minimum and maximum values and pattern in Kelvin for the given channel UID
*/
public void setMinMaxKelvin(ChannelUID channelUID, long minKelvin, long maxKelvin) {
StateDescriptionFragment oldStateDescriptionFragment = stateDescriptionFragments.get(channelUID);
StateDescriptionFragment newStateDescriptionFragment = StateDescriptionFragmentBuilder.create()
.withMinimum(BigDecimal.valueOf(minKelvin)).withMaximum(BigDecimal.valueOf(maxKelvin))
.withStep(BigDecimal.valueOf(100)).withPattern("%.0f K").build();
if (!newStateDescriptionFragment.equals(oldStateDescriptionFragment)) {
stateDescriptionFragments.put(channelUID, newStateDescriptionFragment);
ItemChannelLinkRegistry itemChannelLinkRegistry = this.itemChannelLinkRegistry;
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(),
newStateDescriptionFragment, oldStateDescriptionFragment));
}
}
}

View File

@ -14,11 +14,21 @@
<label>MAC Address</label>
<description>MAC Address of the device</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s">
<parameter name="refreshInterval" type="integer" unit="s" min="2" max="60" step="1">
<label>Light Refresh Interval</label>
<description>The amount of time that passes until the device is refreshed (in seconds)</description>
<default>2</default>
</parameter>
<parameter name="minKelvin" type="integer" unit="K" required="false" min="2000" max="9000" step="100">
<label>Minimum Color Temperature</label>
<description>The minimum color temperature that the light supports (in Kelvin)</description>
<advanced>true</advanced>
</parameter>
<parameter name="maxKelvin" type="integer" unit="K" required="false" min="2000" max="9000" step="100">
<label>Maximum Color Temperature</label>
<description>The maximum color temperature that the light supports (in Kelvin)</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -1,5 +1,28 @@
# add-on
addon.govee.name = Govee Lan-API Binding
addon.govee.description = This is the binding for handling Govee Lights via the LAN-API interface.
# thing types
thing-type.govee.govee-light.label = Govee Light
thing-type.govee.govee-light.description = Govee light controllable via LAN API
# thing types config
thing-type.config.govee.govee-light.hostname.label = Hostname/IP Address
thing-type.config.govee.govee-light.hostname.description = Hostname or IP address of the device
thing-type.config.govee.govee-light.macAddress.label = MAC Address
thing-type.config.govee.govee-light.macAddress.description = MAC Address of the device
thing-type.config.govee.govee-light.maxKelvin.label = Maximum Color Temperature
thing-type.config.govee.govee-light.maxKelvin.description = The maximum color temperature that the light supports (in Kelvin)
thing-type.config.govee.govee-light.minKelvin.label = Minimum Color Temperature
thing-type.config.govee.govee-light.minKelvin.description = The minimum color temperature that the light supports (in Kelvin)
thing-type.config.govee.govee-light.refreshInterval.label = Light Refresh Interval
thing-type.config.govee.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed (in seconds)
# add-on
addon.name = Govee Binding
addon.description = This is the binding for handling Govee Lights via the LAN-API interface.
@ -15,68 +38,127 @@ thing-type.config.govee-light.refreshInterval.description = The amount of time t
# product names
discovery.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights
discovery.govee-light.H6042 = H6042 Govee TV Light Bar #2
discovery.govee-light.H6043 = H6043 Govee TV Light Bars #2
discovery.govee-light.H6046 = H6046 RGBIC TV Light Bars
discovery.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller
discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp
discovery.govee-light.H6052 = H6052 Govee Table Lamp
discovery.govee-light.H6056 = H6056 H6056 Flow Plus
discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids
discovery.govee-light.H6061 = H6061 Glide Hexa LED Panels
discovery.govee-light.H6062 = H6062 Glide Wall Light
discovery.govee-light.H6063 = H6063 Gaming Wall Light
discovery.govee-light.H6065 = H6065 Glide RGBIC Y Lights
discovery.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel
discovery.govee-light.H6067 = H6067 Glide Triangle Light Panels
discovery.govee-light.H606A = H606A Glide Hexa Light Panel Ultra
discovery.govee-light.H6072 = H6072 RGBICWW Corner Floor Lamp
discovery.govee-light.H6076 = H6076 RGBICW Smart Corner Floor Lamp
discovery.govee-light.H6073 = H6073 LED Floor Lamp
discovery.govee-light.H6076 = H6076 RGBICW Smart Corner Floor Lamp
discovery.govee-light.H6078 = H6078 Cylinder Floor Lamp
discovery.govee-light.H607C = H607C Floor Lamp #2
discovery.govee-light.H6087 = H6087 RGBIC Smart Wall Sconces
discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights
discovery.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M
discovery.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating
discovery.govee-light.H619C = H619C LED Strip Lights With Protective Coating
discovery.govee-light.H619D = H619D RGBIC PRO LED Strip Lights
discovery.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating
discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M
discovery.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M
discovery.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M
discovery.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light
discovery.govee-light.H61D3 = H61D3 Neon Rope Light 2 3m
discovery.govee-light.H61D5 = H61D5 Neon Rope Light 2 5m
discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10
discovery.govee-light.H61A8 = H61A8 Neon Rope Light 10
discovery.govee-light.H618A = H618A RGBIC Basic LED Strip Lights 5M
discovery.govee-light.H618C = H618C RGBIC Basic LED Strip Lights 5M
discovery.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M
discovery.govee-light.H6159 = H6159 RGB Light Strip
discovery.govee-light.H615E = H615E LED Strip Lights 30M
discovery.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M
discovery.govee-light.H6088 = H6088 RGBIC Cube Wall Sconces
discovery.govee-light.H608A = H608A String Downlights 5M
discovery.govee-light.H608B = H608B String Downlights 3M
discovery.govee-light.H608C = H608C String Downlights 2M
discovery.govee-light.H608D = H608D String Downlights 10M
discovery.govee-light.H60A0 = H60A0 Ceiling Light
discovery.govee-light.H60A1 = H60A1 Smart Ceiling Light
discovery.govee-light.H610A = H610A Glide Lively Wall Lights
discovery.govee-light.H610B = H610B Music Wall Lights
discovery.govee-light.H6110 = H6110 2x5M Multicolor with Alexa
discovery.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M
discovery.govee-light.H6141 = H6141 5M Smart Multicolor Strip Light
discovery.govee-light.H6143 = H6143 5M Strip Light
discovery.govee-light.H6144 = H6144 2x5M Strip Light
discovery.govee-light.H6159 = H6159 RGB Light Strip
discovery.govee-light.H615A = H615A 5M Light Strip with Alexa
discovery.govee-light.H615B = H615B 10M Light Strip with Alexa
discovery.govee-light.H615C = H615C 15M Light Strip with Alexa
discovery.govee-light.H615D = H615D 20M Light Strip with Alexa
discovery.govee-light.H615E = H615E 30M Light Strip with Alexa
discovery.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M
discovery.govee-light.H6167 = H6167 TV Backlight 2.4M
discovery.govee-light.H6168 = H6168 TV Backlight 2x0.7M+2x1.2M
discovery.govee-light.H616C = H616C Outdoor Strip 10M
discovery.govee-light.H616D = H616D Outdoor Strip 2x7.5M
discovery.govee-light.H616E = H616E Outdoor Strip 2x10M
discovery.govee-light.H6172 = H6172 Outdoor LED Strip 10m
discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight
discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights
discovery.govee-light.H6175 = H6175 RGBIC Outdoor Strip Lights 10M
discovery.govee-light.H6176 = H6176 RGBIC Outdoor Strip Lights 30M
discovery.govee-light.H6182 = H6182 WiFi Multicolor TV Strip Light
discovery.govee-light.H618A = H618A RGBIC Basic LED Strip Lights 5M
discovery.govee-light.H618C = H618C RGBIC Basic LED Strip Lights 5M
discovery.govee-light.H618E = H618E LED Strip Lights 22m
discovery.govee-light.H618F = H618F RGBIC LED Strip Lights
discovery.govee-light.H619A = H619A Strip Lights With Protective Coating 5M
discovery.govee-light.H619B = H619B Strip Lights With Protective Coating 7.5M
discovery.govee-light.H619C = H619C Strip Lights With Protective Coating with Alexa 10M
discovery.govee-light.H619D = H619D PRO LED Strip Lights with Alexa 2x7.5M
discovery.govee-light.H619E = H619E Strip Lights With Protective Coating with Alexa 2x10M
discovery.govee-light.H619Z = H619Z Pro LED Strip Lights 3M
discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 3M
discovery.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M
discovery.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M
discovery.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light 4M
discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10M
discovery.govee-light.H61A8 = H61A8 Neon Rope Light 10M
discovery.govee-light.H61A9 = H61A8 Neon Rope Light 20M
discovery.govee-light.H61B1 = H61B1 Strip Light with Cover 5M
discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight 3M
discovery.govee-light.H61BA = H61BA LED Strip Light 5M
discovery.govee-light.H61BC = H61BC LED Strip Light 10M
discovery.govee-light.H61BE = H61BE LED Strip Light 2x10M
discovery.govee-light.H61C2 = H61C2 Neon LED Strip Light 2M
discovery.govee-light.H61C3 = H61C2 Neon LED Strip Light 3M
discovery.govee-light.H61C5 = H61C2 Neon LED Strip Light 5M
discovery.govee-light.H61D3 = H61D3 Neon Rope Light 2 3m
discovery.govee-light.H61D5 = H61D5 Neon Rope Light 2 5m
discovery.govee-light.H61E0 = H61E0 LED Strip Light M1
discovery.govee-light.H61E1 = H61E1 LED Strip Light M1
discovery.govee-light.H7012 = H7012 Warm White Outdoor String Lights
discovery.govee-light.H7013 = H7013 Warm White Outdoor String Lights
discovery.govee-light.H7021 = H7021 RGBIC Warm White Smart Outdoor String
discovery.govee-light.H7028 = H7028 Lynx Dream LED-Bulb String
discovery.govee-light.H7033 = H7033 LED-Bulb String Lights
discovery.govee-light.H7041 = H7041 LED Outdoor Bulb String Lights
discovery.govee-light.H7042 = H7042 LED Outdoor Bulb String Lights
discovery.govee-light.H705A = H705A Permanent Outdoor Lights 30M
discovery.govee-light.H705B = H705B Permanent Outdoor Lights 15M
discovery.govee-light.H7050 = H7050 Outdoor Ground Lights 11M
discovery.govee-light.H7051 = H7051 Outdoor Ground Lights 15M
discovery.govee-light.H7052 = H7052 Outdoor Ground Lights 15M
discovery.govee-light.H7053 = H7052 Outdoor Ground Lights 30M
discovery.govee-light.H7055 = H7055 Pathway Light
discovery.govee-light.H705A = H705A Permanent Outdoor Lights 30M
discovery.govee-light.H705B = H705B Permanent Outdoor Lights 15M
discovery.govee-light.H705C = H705C Permanent Outdoor Lights 45M
discovery.govee-light.H705D = H705D Permanent Outdoor Lights #2 15M
discovery.govee-light.H705E = H705E Permanent Outdoor Lights #2 30M
discovery.govee-light.H705F = H705F Permanent Outdoor Lights #2 45M
discovery.govee-light.H7060 = H7060 LED Flood Lights (2-Pack)
discovery.govee-light.H7061 = H7061 LED Flood Lights (4-Pack)
discovery.govee-light.H7062 = H7062 LED Flood Lights (6-Pack)
discovery.govee-light.H7063 = H7063 Outdoor Flood Lights
discovery.govee-light.H7065 = H7065 Outdoor Spot Lights
discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp
discovery.govee-light.H6056 = H6056 H6056 Flow Plus
discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids
discovery.govee-light.H618F = H618F RGBIC LED Strip Lights
discovery.govee-light.H618E = H618E LED Strip Lights 22m
discovery.govee-light.H6168 = H6168 TV LED Backlight
discovery.govee-light.H7066 = H7066 Outdoor Spot Lights
discovery.govee-light.H706A = H706A Permanent Outdoor Lights Pro 30M
discovery.govee-light.H706B = H706B Permanent Outdoor Lights Pro 45M
discovery.govee-light.H706C = H706C Permanent Outdoor Lights Pro 60M
discovery.govee-light.H7070 = H7070 Outdoor Projector Light
discovery.govee-light.H7075 = H7075 Outdoor Wall Light
discovery.govee-light.H70B1 = H70B1 520 LED Curtain Lights
discovery.govee-light.H70BC = H70BC 400 LED Curtain Lights
discovery.govee-light.H70C1 = H70C1 RGBIC String Light 10M
discovery.govee-light.H70C2 = H70C2 RGBIC String Light 20M
discovery.govee-light.H805A = H805A Permanent Outdoor Lights Elite 30M
discovery.govee-light.H805B = H805B Permanent Outdoor Lights Elite 15M
discovery.govee-light.H805C = H805C Permanent Outdoor Lights Elite 45M
# thing status descriptions
offline.communication-error.could-not-query-device = Could not control/query device at IP address {0}
offline.configuration-error.ip-address.missing = IP address is missing
offline.communication-error.empty-response = Empty response received
offline.configuration-error.invalid-color-temperature-range = Invalid color temperature range

View File

@ -11,22 +11,14 @@
<channels>
<channel id="color" typeId="system.color"/>
<channel id="color-temperature" typeId="system.color-temperature"/>
<channel id="color-temperature-abs" typeId="color-temperature-abs"/>
<channel id="color-temperature-abs" typeId="system.color-temperature-abs"/>
</channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description-ref uri="thing-type:govee:govee-light"/>
</thing-type>
<channel-type id="color-temperature-abs" advanced="true">
<item-type unitHint="K">Number:Temperature</item-type>
<label>Color Temperature</label>
<description>Controls the color temperature of the light in Kelvin</description>
<category>ColorLight</category>
<tags>
<tag>Control</tag>
<tag>ColorTemperature</tag>
</tags>
<state min="2000" max="9000" pattern="%.0f K"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="govee:govee-light">
<instruction-set targetVersion="1">
<update-channel id="color-temperature-abs">
<type>system:color-temperature-abs</type>
</update-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>

View File

@ -12,8 +12,7 @@
*/
package org.openhab.binding.govee.internal;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doAnswer;
import java.util.concurrent.ScheduledExecutorService;
@ -26,14 +25,15 @@ import org.openhab.core.thing.Thing;
/**
* The {@link GoveeHandlerMock} is responsible for mocking {@link GoveeHandler}
*
*
* @author Leo Siepel - Initial contribution
*/
@NonNullByDefault
public class GoveeHandlerMock extends GoveeHandler {
public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager) {
super(thing, communicationManager);
public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager,
GoveeStateDescriptionProvider stateDescriptionProvider) {
super(thing, communicationManager, stateDescriptionProvider);
executorService = Mockito.mock(ScheduledExecutorService.class);
doAnswer((InvocationOnMock invocation) -> {

View File

@ -12,12 +12,8 @@
*/
package org.openhab.binding.govee.internal;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.List;
@ -87,7 +83,10 @@ public class GoveeSerializeGoveeHandlerTest {
private static GoveeHandlerMock createAndInitHandler(final ThingHandlerCallback callback, final Thing thing) {
CommunicationManager communicationManager = mock(CommunicationManager.class);
final GoveeHandlerMock handler = spy(new GoveeHandlerMock(thing, communicationManager));
GoveeStateDescriptionProvider stateDescriptionProvider = mock(GoveeStateDescriptionProvider.class);
final GoveeHandlerMock handler = spy(
new GoveeHandlerMock(thing, communicationManager, stateDescriptionProvider));
handler.setCallback(callback);
handler.initialize();
@ -135,7 +134,7 @@ public class GoveeSerializeGoveeHandlerTest {
verify(callback).stateUpdated(
new ChannelUID(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS),
getState(2000, Units.KELVIN));
getState(9000, Units.KELVIN));
} finally {
handler.dispose();
}