[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) 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 - H6046 RGBIC TV Light Bars
- H6047 RGBIC Gaming Light Bars with Smart Controller - 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 (*) - H6061 Glide Hexa LED Panels (*)
- H6062 Glide Wall Light - H6062 Glide Wall Light
- H6063 Gaming Wall Light
- H6065 Glide RGBIC Y Lights - H6065 Glide RGBIC Y Lights
- H6066 Glide Hexa Pro LED Panel - H6066 Glide Hexa Pro LED Panel
- H6067 Glide Triangle Light Panels (*) - H6067 Glide Triangle Light Panels (*)
- H606A Glide Hexa Light Panel Ultra
- H6072 RGBICWW Corner Floor Lamp (*) - H6072 RGBICWW Corner Floor Lamp (*)
- H6076 RGBICW Smart Corner Floor Lamp (*)
- H6073 LED Floor Lamp - H6073 LED Floor Lamp
- H6076 RGBICW Smart Corner Floor Lamp (*)
- H6078 Cylinder Floor Lamp - H6078 Cylinder Floor Lamp
- H607C Floor Lamp #2
- H6087 RGBIC Smart Wall Sconces - H6087 RGBIC Smart Wall Sconces
- H6173 RGBIC Outdoor Strip Lights - H6088 RGBIC Cube Wall Sconces
- H619A RGBIC Strip Lights With Protective Coating 5M - H608A String Downlights 5M
- H619B RGBIC LED Strip Lights With Protective Coating - H608B String Downlights 3M
- H619C LED Strip Lights With Protective Coating - H608C String Downlights 2M
- H619D RGBIC PRO LED Strip Lights - H608D String Downlights 10M
- H619E RGBIC LED Strip Lights With Protective Coating - H60A0 Ceiling Light
- H61A0 RGBIC Neon Rope Light 1M - H60A1 Smart Ceiling Light (*)
- 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
- H610A Glide Lively Wall Lights - H610A Glide Lively Wall Lights
- H610B Music 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 - 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 - H61E1 LED Strip Light M1
- H7012 Warm White Outdoor String Lights - H7012 Warm White Outdoor String Lights
- H7013 Warm White Outdoor String Lights - H7013 Warm White Outdoor String Lights
- H7021 RGBIC Warm White Smart Outdoor String - H7021 RGBIC Warm White Smart Outdoor String
- H7028 Lynx Dream LED-Bulb String - H7028 Lynx Dream LED-Bulb String
- H7033 LED-Bulb String Lights
- H7041 LED Outdoor Bulb String Lights - H7041 LED Outdoor Bulb String Lights
- H7042 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 - H7050 Outdoor Ground Lights 11M
- H7051 Outdoor Ground Lights 15M - H7051 Outdoor Ground Lights 15M
- H7052 Outdoor Ground Lights 15M
- H7052 Outdoor Ground Lights 30M
- H7055 Pathway Light - 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) - H7060 LED Flood Lights (2-Pack)
- H7061 LED Flood Lights (4-Pack) - H7061 LED Flood Lights (4-Pack)
- H7062 LED Flood Lights (6-Pack) - H7062 LED Flood Lights (6-Pack)
- H7063 Outdoor Flood Lights
- H7065 Outdoor Spot Lights - H7065 Outdoor Spot Lights
- H70C1 Govee Christmas String Lights 10m (*) - H7066 Outdoor Spot Lights
- H70C2 Govee Christmas String Lights 20m (*) - H706A Permanent Outdoor Lights Pro 30M
- H6051 Aura - Smart Table Lamp - H706B Permanent Outdoor Lights Pro 45M
- H6056 H6056 Flow Plus - H706C Permanent Outdoor Lights Pro 60M
- H6059 RGBWW Night Light for Kids - H7070 Outdoor Projector Light (*)
- H618F RGBIC LED Strip Lights - H7075 Outdoor Wall Light
- H618E LED Strip Lights 22m - H70B1 520 LED Curtain Lights
- H6168 TV LED Backlight - 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
Discovery is done by scanning the devices in the Thing section. Discovery is done by scanning the devices in the Thing section.
@ -108,11 +173,13 @@ arp -a | grep "MAC_ADDRESS"
### `govee-light` Thing Configuration ### `govee-light` Thing Configuration
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------|---------|----------|----------| |-----------------|---------|------------------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no | | hostname | text | Hostname or IP address of the device | N/A | yes | no |
| macAddress | text | MAC 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 | | 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 ## Channels

View File

@ -15,13 +15,23 @@ package org.openhab.binding.govee.internal;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface; 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.time.Instant;
import java.util.HashMap; import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -36,231 +47,259 @@ import com.google.gson.Gson;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
/** /**
* The {@link CommunicationManager} is a thread that handles the answers of all devices. * The {@link CommunicationManager} component implements a sender to send commands to Govee devices,
* Therefore it needs to apply the information to the right thing. * 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
* Discovery uses the same response code, so we must not refresh the status during discovery. * concurrently.
* *
* @author Stefan Höhn - Initial contribution * @author Stefan Höhn - Initial contribution
* @author Danny Baumann - Thread-Safe design refactoring * @author Danny Baumann - Thread-Safe design refactoring
* @author Andrew Fiddian-Green - New threading model using java.nio channel
*/ */
@NonNullByDefault @NonNullByDefault
@Component(service = CommunicationManager.class) @Component(service = CommunicationManager.class)
public class CommunicationManager { public class CommunicationManager {
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class); private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
private final Gson gson = new Gson(); 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<>(); // list of Thing handler listeners that will receive state notifications
@Nullable private final Map<String, GoveeHandler> thingHandlerListeners = new ConcurrentHashMap<>();
private StatusReceiver receiverThread;
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 String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250";
private static final int DISCOVERY_PORT = 4001; private static final int DISCOVERY_PORT = 4001;
private static final int RESPONSE_PORT = 4002; private static final int RESPONSE_PORT = 4002;
private static final int REQUEST_PORT = 4003; 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\"}}}"; private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";
public interface DiscoveryResultReceiver { private static final InetSocketAddress DISCOVERY_SOCKET_ADDRESS = new InetSocketAddress(DISCOVERY_MULTICAST_ADDRESS,
void onResultReceived(DiscoveryResponse result); DISCOVERY_PORT);
public interface GoveeDiscoveryListener {
void onDiscoveryResponse(DiscoveryResponse discoveryResponse);
} }
@Activate @Activate
public CommunicationManager() { 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) { public void registerHandler(GoveeHandler handler) {
synchronized (thingHandlers) { thingHandlerListeners.put(ipAddressFrom(handler.getHostname()), handler);
thingHandlers.put(handler.getHostname(), handler);
if (receiverThread == null) {
receiverThread = new StatusReceiver();
receiverThread.start();
}
}
} }
/**
* Thing handlers unregister themselves when they are destroyed.
*/
public void unregisterHandler(GoveeHandler handler) { public void unregisterHandler(GoveeHandler handler) {
synchronized (thingHandlers) { thingHandlerListeners.remove(ipAddressFrom(handler.getHostname()));
thingHandlers.remove(handler.getHostname());
if (thingHandlers.isEmpty()) {
StatusReceiver receiver = receiverThread;
if (receiver != null) {
receiver.stopReceiving();
}
receiverThread = null;
}
}
} }
/**
* Send a unicast command request to the device.
*/
public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException { public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException {
final String hostname = handler.getHostname(); serverStart();
final DatagramSocket socket = new DatagramSocket(); synchronized (senderLock) {
socket.setReuseAddress(true); try (DatagramSocket socket = new DatagramSocket()) {
final String message = gson.toJson(request); socket.setReuseAddress(true);
final byte[] data = message.getBytes(); String message = gson.toJson(request);
final InetAddress address = InetAddress.getByName(hostname); byte[] data = message.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT); String hostname = handler.getHostname();
logger.trace("Sending {} to {}", message, hostname); InetAddress address = InetAddress.getByName(hostname);
socket.send(packet); DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
socket.close(); socket.send(packet);
} logger.trace("Sent request to {} on {} with content = {}", handler.getThing().getUID(),
address.getHostAddress(), message);
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();
}
} }
} }
} }
private class StatusReceiver extends Thread { /**
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class); * Send discovery multicast pings on any ipv4 address bound to any network interface in the given
private boolean stopped = false; * list and then sleep for sufficient time until responses may have been received.
private @Nullable DiscoveryResultReceiver discoveryResultReceiver; */
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() { Duration sleepDuration = Duration.between(Instant.now(), sleepUntil);
super("GoveeStatusReceiver"); if (!sleepDuration.isNegative()) {
}
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) {
try { try {
socket = new MulticastSocket(RESPONSE_PORT); Thread.sleep(sleepDuration.toMillis());
byte[] buffer = new byte[10240]; } catch (InterruptedException e) {
socket.setReuseAddress(true); // just return
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;
}
} }
} }
} 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 PRODUCT_NAME = "productName";
public static final String HW_VERSION = "wifiHardwareVersion"; public static final String HW_VERSION = "wifiHardwareVersion";
public static final String SW_VERSION = "wifiSoftwareVersion"; 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 // List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light"); public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light");
@ -44,4 +44,7 @@ public class GoveeBindingConstants {
// Limit values of channels // Limit values of channels
public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0; public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0;
public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.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; package org.openhab.binding.govee.internal;
import org.eclipse.jdt.annotation.NonNullByDefault; 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 * The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler
@ -24,4 +25,7 @@ public class GoveeConfiguration {
public String hostname = ""; public String hostname = "";
public int refreshInterval = 5; // in seconds 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; package org.openhab.binding.govee.internal;
import java.io.IOException;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.SocketException; import java.net.SocketException;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.DiscoveryData;
import org.openhab.binding.govee.internal.model.DiscoveryResponse; import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.AbstractDiscoveryService;
@ -79,17 +81,22 @@ import org.slf4j.LoggerFactory;
*/ */
@NonNullByDefault @NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee") @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 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); private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
@Activate @Activate
public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider, public GoveeDiscoveryService(final @Reference TranslationProvider i18nProvider,
@Reference CommunicationManager communicationManager) { final @Reference LocaleProvider localeProvider,
super(SUPPORTED_THING_TYPES_UIDS, 0, false); final @Reference CommunicationManager communicationManager) {
super(SUPPORTED_THING_TYPES_UIDS, CommunicationManager.SCAN_TIMEOUT_SEC, true);
this.i18nProvider = i18nProvider; this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider; this.localeProvider = localeProvider;
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
@ -103,23 +110,8 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
@Override @Override
protected void startScan() { protected void startScan() {
logger.debug("starting Scan"); logger.debug("Starting scan");
scheduler.schedule(this::doDiscovery, 0, TimeUnit.MILLISECONDS);
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");
});
} }
public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) { public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
@ -165,11 +157,11 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
} }
String hwVersion = data.wifiVersionHard(); String hwVersion = data.wifiVersionHard();
if (hwVersion != null) { if (!hwVersion.isEmpty()) {
builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion); builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
} }
String swVersion = data.wifiVersionSoft(); String swVersion = data.wifiVersionSoft();
if (swVersion != null) { if (!swVersion.isEmpty()) {
builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion); builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
} }
@ -194,4 +186,41 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
} }
return result; 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 static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
import java.io.IOException; 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.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; 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.Color;
import org.openhab.binding.govee.internal.model.ColorData; import org.openhab.binding.govee.internal.model.ColorData;
import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData; 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.GenericGoveeMsg;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest; import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.openhab.binding.govee.internal.model.StatusResponse; import org.openhab.binding.govee.internal.model.StatusResponse;
import org.openhab.binding.govee.internal.model.ValueIntData; 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.HSBType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType; 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.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.ColorUtil; import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -71,47 +79,73 @@ import com.google.gson.JsonSyntaxException;
* https://app-h5.govee.com/user-manual/wlan-guide * https://app-h5.govee.com/user-manual/wlan-guide
* *
* @author Stefan Höhn - Initial contribution * @author Stefan Höhn - Initial contribution
* @author Andrew Fiddian-Green - Added sequential task processing
*/ */
@NonNullByDefault @NonNullByDefault
public class GoveeHandler extends BaseThingHandler { public class GoveeHandler extends BaseThingHandler {
/*
* Messages to be sent to the Govee devices
*/
private static final Gson GSON = new Gson(); 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); private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
protected ScheduledExecutorService executorService = scheduler; protected ScheduledExecutorService executorService = scheduler;
@Nullable private @Nullable ScheduledFuture<?> thingTaskSenderTask;
private ScheduledFuture<?> triggerStatusJob; // send device status update job
private GoveeConfiguration goveeConfiguration = new GoveeConfiguration(); 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 OnOffType lastSwitch = OnOffType.OFF;
private int lastBrightness;
private HSBType lastColor = new HSBType(); 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. * This thing related job <i>thingTaskSender</i> sends the next queued command (if any)
* The device sends it back to the common port and the response is * to the Govee device. If there is no queued command and a regular refresh is due then
* then received by the common #refreshStatusReceiver * 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 = () -> { private final Runnable thingTaskSender = () -> {
try { synchronized (taskQueue) {
triggerDeviceStatusRefresh(); if (taskQueue.isEmpty() && Instant.now().isBefore(nextRefreshDueTime)) {
updateStatus(ThingStatus.ONLINE); return; // no queued command nor pending refresh
} catch (IOException e) { }
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, if (taskQueue.isEmpty()) {
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname 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); super(thing);
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
this.stateDescriptionProvider = stateDescriptionProvider;
} }
public String getHostname() { public String getHostname() {
@ -128,140 +162,176 @@ public class GoveeHandler extends BaseThingHandler {
"@text/offline.configuration-error.ip-address.missing"); "@text/offline.configuration-error.ip-address.missing");
return; 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); updateStatus(ThingStatus.UNKNOWN);
communicationManager.registerHandler(this); communicationManager.registerHandler(this);
if (triggerStatusJob == null) {
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
triggerStatusJob = executorService.scheduleWithFixedDelay(thingRefreshSender, 100, if (thingTaskSenderTask == null) {
goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS); 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 @Override
public void dispose() { public void dispose() {
super.dispose(); super.dispose();
taskQueue.clear();
ScheduledFuture<?> triggerStatusJobFuture = triggerStatusJob; ScheduledFuture<?> job = thingTaskSenderTask;
if (triggerStatusJobFuture != null) { if (job != null) {
triggerStatusJobFuture.cancel(true); job.cancel(true);
triggerStatusJob = null; thingTaskSenderTask = null;
} }
communicationManager.unregisterHandler(this); communicationManager.unregisterHandler(this);
} }
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command commandParam) {
try { Command command = commandParam;
synchronized (taskQueue) {
logger.debug("handleCommand({}, {})", channelUID, command);
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
// we are refreshing all channels at once, as we get all information at the same time taskQueue.add(() -> triggerDeviceStatusRefresh());
triggerDeviceStatusRefresh();
logger.debug("Triggering Refresh");
} else { } else {
logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass());
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_COLOR: case CHANNEL_COLOR:
if (command instanceof HSBType hsbCommand) { if (command instanceof HSBType hsb) {
int[] rgb = ColorUtil.hsbToRgb(hsbCommand); taskQueue.add(() -> sendColor(hsb));
sendColor(new Color(rgb[0], rgb[1], rgb[2])); command = hsb.getBrightness(); // fall through
} else if (command instanceof PercentType percent) { }
sendBrightness(percent.intValue()); if (command instanceof PercentType percent) {
} else if (command instanceof OnOffType onOffCommand) { taskQueue.add(() -> sendBrightness(percent));
sendOnOff(onOffCommand); command = OnOffType.from(percent.intValue() > 0); // fall through
}
if (command instanceof OnOffType onOff) {
taskQueue.add(() -> sendOnOff(onOff));
taskQueue.add(() -> triggerDeviceStatusRefresh());
} }
break; break;
case CHANNEL_COLOR_TEMPERATURE: case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof PercentType percent) { if (command instanceof PercentType percent) {
logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", command); taskQueue.add(() -> sendKelvin(percentToKelvin(percent)));
Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.intValue() taskQueue.add(() -> triggerDeviceStatusRefresh());
* (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0);
lastColorTempInKelvin = colorTemp.intValue();
logger.debug("lastColorTempInKelvin {}", lastColorTempInKelvin);
sendColorTemp(lastColorTempInKelvin);
} }
break; break;
case CHANNEL_COLOR_TEMPERATURE_ABS: case CHANNEL_COLOR_TEMPERATURE_ABS:
if (command instanceof QuantityType<?> quantity) { if (command instanceof QuantityType<?> genericQuantity) {
logger.debug("Color Temperature Absolute change with Percent Type {}", command); QuantityType<?> kelvin = genericQuantity.toInvertibleUnit(Units.KELVIN);
lastColorTempInKelvin = quantity.intValue(); if (kelvin == null) {
logger.debug("COLOR_TEMPERATURE_ABS: lastColorTempInKelvin {}", lastColorTempInKelvin); logger.warn("handleCommand() invalid QuantityType:{}", genericQuantity);
int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin break;
- COLOR_TEMPERATURE_MIN_VALUE) }
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue(); taskQueue.add(() -> sendKelvin(kelvin.intValue()));
logger.debug("computed lastColorTempInPercent {}", lastColorTempInPercent); taskQueue.add(() -> triggerDeviceStatusRefresh());
sendColorTemp(lastColorTempInKelvin); } else if (command instanceof DecimalType kelvin) {
taskQueue.add(() -> sendKelvin(kelvin.intValue()));
taskQueue.add(() -> triggerDeviceStatusRefresh());
} }
break; 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 { private boolean triggerDeviceStatusRefresh() throws IOException {
logger.debug("trigger Refresh Status of device {}", thing.getLabel()); logger.debug("triggerDeviceStatusRefresh() to {}", thing.getUID());
GenericGoveeRequest lightQuery = new GenericGoveeRequest( GenericGoveeData data = new EmptyValueQueryStatusData();
new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("devStatus", data));
communicationManager.sendRequest(this, lightQuery); communicationManager.sendRequest(this, request);
} return true;
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);
} }
/** /**
* Creates a Color state by using the last color information from lastColor * Send the normalized RGB color parameters.
* 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
*/ */
private HSBType getColorState(Color color, int brightness) { public boolean sendColor(HSBType color) throws IOException {
PercentType computedBrightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(brightness); logger.debug("sendColor({}) to {}", color, thing.getUID());
int[] rgb = { color.r(), color.g(), color.b() }; int[] normalRGB = ColorUtil.hsbToRgb(new HSBType(color.getHue(), color.getSaturation(), PercentType.HUNDRED));
HSBType hsb = ColorUtil.rgbToHsb(rgb); GenericGoveeData data = new ColorData(new Color(normalRGB[0], normalRGB[1], normalRGB[2]), 0);
return new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness); 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()) { if (response.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.empty-response"); "@text/offline.communication-error.empty-response");
@ -284,47 +354,52 @@ public class GoveeHandler extends BaseThingHandler {
return; return;
} }
logger.trace("Receiving Device State"); logger.debug("updateDeviceState() for {}", thing.getUID());
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);
newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE) OnOffType sw = OnOffType.from(message.msg().data().onOff() == 1);
? COLOR_TEMPERATURE_MIN_VALUE.intValue() int brightness = message.msg().data().brightness();
: newColorTempInKelvin; Color normalRGB = message.msg().data().color();
newColorTempInKelvin = (newColorTempInKelvin > COLOR_TEMPERATURE_MAX_VALUE) int kelvin = message.msg().data().colorTemInKelvin();
? COLOR_TEMPERATURE_MAX_VALUE.intValue()
: newColorTempInKelvin;
int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE) logger.trace("Update values: switch:{}, brightness:{}, normalRGB:{}, kelvin:{}", sw, brightness, normalRGB,
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue(); kelvin);
HSBType adaptedColor = getColorState(newColor, newBrightness); HSBType color = buildHSB(normalRGB, brightness, true);
logger.trace("HSB old: {} vs adaptedColor: {}", lastColor, adaptedColor); logger.trace("Compare hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
// avoid noise by only updating if the value has changed on the device if ((sw != lastSwitch) || !color.equals(lastColor)) {
if (!adaptedColor.equals(lastColor)) { logger.trace("Update hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
logger.trace("UPDATING HSB old: {} != {}", lastColor, adaptedColor); updateState(CHANNEL_COLOR, buildHSB(normalRGB, brightness, sw == OnOffType.ON));
updateState(CHANNEL_COLOR, adaptedColor); lastSwitch = sw;
lastColor = color;
} }
// avoid noise by only updating if the value has changed on the device logger.trace("Compare kelvin old:{} to new:{}", lastKelvin, kelvin);
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin, if (kelvin != lastKelvin) {
newColorTempInPercent, newColorTempInKelvin); logger.trace("Update kelvin old:{} to new:{}", lastKelvin, kelvin);
if (newColorTempInKelvin != lastColorTempInKelvin) { if (kelvin != 0) {
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin, kelvin = Math.round(Math.min(maxKelvin, Math.max(minKelvin, kelvin)));
newColorTempInPercent, newColorTempInKelvin); updateState(CHANNEL_COLOR_TEMPERATURE, kelvinToPercent(kelvin));
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType<>(lastColorTempInKelvin, Units.KELVIN)); updateState(CHANNEL_COLOR_TEMPERATURE_ABS, QuantityType.valueOf(kelvin, Units.KELVIN));
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(newColorTempInPercent)); } else {
updateState(CHANNEL_COLOR_TEMPERATURE, UnDefType.UNDEF);
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, UnDefType.UNDEF);
}
lastKelvin = kelvin;
} }
}
lastOnOff = newOnOff; /**
lastColor = adaptedColor; * Convert PercentType to Kelvin.
lastBrightness = newBrightness; */
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 { public class GoveeHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT); 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 @Activate
public GoveeHandlerFactory(@Reference CommunicationManager communicationManager) { public GoveeHandlerFactory(final @Reference CommunicationManager communicationManager,
final @Reference GoveeStateDescriptionProvider stateDescriptionProvider) {
this.communicationManager = communicationManager; this.communicationManager = communicationManager;
this.stateDescriptionProvider = stateDescriptionProvider;
} }
@Override @Override
@ -55,7 +58,7 @@ public class GoveeHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_LIGHT.equals(thingTypeUID)) { if (THING_TYPE_LIGHT.equals(thingTypeUID)) {
return new GoveeHandler(thing, communicationManager); return new GoveeHandler(thing, communicationManager, stateDescriptionProvider);
} }
return null; 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> <label>MAC Address</label>
<description>MAC Address of the device</description> <description>MAC Address of the device</description>
</parameter> </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> <label>Light Refresh Interval</label>
<description>The amount of time that passes until the device is refreshed (in seconds)</description> <description>The amount of time that passes until the device is refreshed (in seconds)</description>
<default>2</default> <default>2</default>
</parameter> </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-description:config-descriptions> </config-description:config-descriptions>

View File

@ -1,5 +1,28 @@
# add-on # 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.name = Govee Binding
addon.description = This is the binding for handling Govee Lights via the LAN-API interface. 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 # 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.H6046 = H6046 RGBIC TV Light Bars
discovery.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller 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.H6061 = H6061 Glide Hexa LED Panels
discovery.govee-light.H6062 = H6062 Glide Wall Light 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.H6065 = H6065 Glide RGBIC Y Lights
discovery.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel discovery.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel
discovery.govee-light.H6067 = H6067 Glide Triangle Light Panels 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.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.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.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.H6087 = H6087 RGBIC Smart Wall Sconces
discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights discovery.govee-light.H6088 = H6088 RGBIC Cube Wall Sconces
discovery.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M discovery.govee-light.H608A = H608A String Downlights 5M
discovery.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating discovery.govee-light.H608B = H608B String Downlights 3M
discovery.govee-light.H619C = H619C LED Strip Lights With Protective Coating discovery.govee-light.H608C = H608C String Downlights 2M
discovery.govee-light.H619D = H619D RGBIC PRO LED Strip Lights discovery.govee-light.H608D = H608D String Downlights 10M
discovery.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating discovery.govee-light.H60A0 = H60A0 Ceiling Light
discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M discovery.govee-light.H60A1 = H60A1 Smart Ceiling Light
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.H610A = H610A Glide Lively Wall Lights discovery.govee-light.H610A = H610A Glide Lively Wall Lights
discovery.govee-light.H610B = H610B Music 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.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.H61E1 = H61E1 LED Strip Light M1
discovery.govee-light.H7012 = H7012 Warm White Outdoor String Lights discovery.govee-light.H7012 = H7012 Warm White Outdoor String Lights
discovery.govee-light.H7013 = H7013 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.H7021 = H7021 RGBIC Warm White Smart Outdoor String
discovery.govee-light.H7028 = H7028 Lynx Dream LED-Bulb 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.H7041 = H7041 LED Outdoor Bulb String Lights
discovery.govee-light.H7042 = H7042 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.H7050 = H7050 Outdoor Ground Lights 11M
discovery.govee-light.H7051 = H7051 Outdoor Ground Lights 15M 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.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.H7060 = H7060 LED Flood Lights (2-Pack)
discovery.govee-light.H7061 = H7061 LED Flood Lights (4-Pack) discovery.govee-light.H7061 = H7061 LED Flood Lights (4-Pack)
discovery.govee-light.H7062 = H7062 LED Flood Lights (6-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.H7065 = H7065 Outdoor Spot Lights
discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp discovery.govee-light.H7066 = H7066 Outdoor Spot Lights
discovery.govee-light.H6056 = H6056 H6056 Flow Plus discovery.govee-light.H706A = H706A Permanent Outdoor Lights Pro 30M
discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids discovery.govee-light.H706B = H706B Permanent Outdoor Lights Pro 45M
discovery.govee-light.H618F = H618F RGBIC LED Strip Lights discovery.govee-light.H706C = H706C Permanent Outdoor Lights Pro 60M
discovery.govee-light.H618E = H618E LED Strip Lights 22m discovery.govee-light.H7070 = H7070 Outdoor Projector Light
discovery.govee-light.H6168 = H6168 TV LED Backlight 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 # thing status descriptions
offline.communication-error.could-not-query-device = Could not control/query device at IP address {0} 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.configuration-error.ip-address.missing = IP address is missing
offline.communication-error.empty-response = Empty response received 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> <channels>
<channel id="color" typeId="system.color"/> <channel id="color" typeId="system.color"/>
<channel id="color-temperature" typeId="system.color-temperature"/> <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> </channels>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description-ref uri="thing-type:govee:govee-light"/> <config-description-ref uri="thing-type:govee:govee-light"/>
</thing-type> </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> </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; package org.openhab.binding.govee.internal;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -32,8 +31,9 @@ import org.openhab.core.thing.Thing;
@NonNullByDefault @NonNullByDefault
public class GoveeHandlerMock extends GoveeHandler { public class GoveeHandlerMock extends GoveeHandler {
public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager) { public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager,
super(thing, communicationManager); GoveeStateDescriptionProvider stateDescriptionProvider) {
super(thing, communicationManager, stateDescriptionProvider);
executorService = Mockito.mock(ScheduledExecutorService.class); executorService = Mockito.mock(ScheduledExecutorService.class);
doAnswer((InvocationOnMock invocation) -> { doAnswer((InvocationOnMock invocation) -> {

View File

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