Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[govee] Fix brightness vs. color synchronization #17812

Merged
merged 38 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5214ab0
[govee] Fix synchronization of brightness
andrewfg Nov 27, 2024
d5daa92
rectacoring
andrewfg Nov 27, 2024
7d49ffd
improve update speed, add property
andrewfg Nov 27, 2024
4dd0420
add new device labels, and sort them
andrewfg Nov 27, 2024
8aa0c96
read me
andrewfg Nov 28, 2024
dcd6aeb
fix host name resolution
andrewfg Nov 28, 2024
ab4a18a
logging tweaks
andrewfg Nov 28, 2024
0acd7db
support configurable color temperature ranges
andrewfg Nov 28, 2024
4c0ca10
refactoring
andrewfg Nov 28, 2024
88afd3d
trigger refresh on separate thread
andrewfg Nov 28, 2024
7d581b9
add inter command sleep
andrewfg Nov 28, 2024
1590fd6
tweak logging
andrewfg Nov 29, 2024
190520f
gnarly thread synchronization code
andrewfg Nov 29, 2024
3f26176
restore * tags
andrewfg Nov 29, 2024
d3a3bf5
avoid potential threadlock
andrewfg Nov 29, 2024
28bbd1d
improved thread synchronization
andrewfg Nov 29, 2024
b9f5384
add (*) tag
andrewfg Nov 30, 2024
fcd032c
improve logging; add properties
andrewfg Nov 30, 2024
46809de
fix discovery on multi head / multi protocol machines
andrewfg Dec 3, 2024
febb897
move asterisk in read me
andrewfg Dec 3, 2024
34d9731
normalize thread name (see #17804)
andrewfg Dec 4, 2024
80ce982
adopt reviewer suggestions
andrewfg Dec 8, 2024
8bf37f7
adopt reviewer suggestion
andrewfg Dec 8, 2024
14280b4
extensive refactoring
andrewfg Dec 11, 2024
7886e93
Merge branch 'openhab:main' into #17807-govee
andrewfg Dec 11, 2024
7f8771b
resequence methods for easier review
andrewfg Dec 11, 2024
14e63d8
improve discovery
andrewfg Dec 12, 2024
d04139e
functional tests done
andrewfg Dec 12, 2024
a64ae1d
eliminate spurious log messages
andrewfg Dec 12, 2024
8a54903
restrict refresh interval to 2 seconds
andrewfg Dec 12, 2024
9a39470
restrict refresh interval to 2 seconds #2
andrewfg Dec 12, 2024
be9f0ba
fix config.xml
andrewfg Dec 12, 2024
9cff94e
increase inter command delay
andrewfg Dec 13, 2024
f02252c
Merge branch 'openhab:main' into #17807-govee
andrewfg Dec 16, 2024
b6595be
improve server thread; add command pipelining
andrewfg Dec 19, 2024
3fef687
Merge branch 'openhab:main' into #17807-govee
andrewfg Dec 20, 2024
be443cf
Update README.md
andrewfg Dec 22, 2024
21b0ffd
adopt reviewer suggestion
andrewfg Dec 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 104 additions & 38 deletions bundles/org.openhab.binding.govee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,68 +20,132 @@ 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
- 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 (*)
- H615E LED Strip Lights 30M
- 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
- H610A Glide Lively Wall Lights
- H610B Music Wall Lights
- 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
- 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:
andrewfg marked this conversation as resolved.
Show resolved Hide resolved

- 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.
Expand All @@ -108,11 +172,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@
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.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -63,6 +69,9 @@ public class CommunicationManager {

private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";

private static final InetSocketAddress DISCOVERY_SOCKET_ADDRESS = new InetSocketAddress(DISCOVERY_MULTICAST_ADDRESS,
DISCOVERY_PORT);

public interface DiscoveryResultReceiver {
void onResultReceived(DiscoveryResponse result);
}
Expand All @@ -71,9 +80,20 @@ public interface DiscoveryResultReceiver {
public CommunicationManager() {
}

/**
* 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;
}

public void registerHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.put(handler.getHostname(), handler);
thingHandlers.put(ipAddressFrom(handler.getHostname()), handler);
if (receiverThread == null) {
receiverThread = new StatusReceiver();
receiverThread.start();
Expand All @@ -83,7 +103,7 @@ public void registerHandler(GoveeHandler handler) {

public void unregisterHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.remove(handler.getHostname());
thingHandlers.remove(ipAddressFrom(handler.getHostname()));
if (thingHandlers.isEmpty()) {
StatusReceiver receiver = receiverThread;
if (receiver != null) {
Expand All @@ -102,7 +122,8 @@ public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throw
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);
logger.trace("Sending request to {} on {} with content = {}", handler.getThing().getUID(),
address.getHostAddress(), message);
socket.send(packet);
socket.close();
}
Expand All @@ -125,24 +146,26 @@ public void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultRecei
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);
final Instant discoveryEndTime = Instant.now().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);
}
Collections.list(intf.getInetAddresses()).stream().filter(address -> address instanceof Inet4Address)
.map(address -> address.getHostAddress()).forEach(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, intf)
.bind(new InetSocketAddress(ipv4Address, DISCOVERY_PORT))
.configureBlocking(false)) {
logger.trace("Datagram channel bound to {}:{} on {}", ipv4Address, DISCOVERY_PORT,
intf.getDisplayName());
channel.send(ByteBuffer.wrap(DISCOVER_REQUEST.getBytes()), DISCOVERY_SOCKET_ADDRESS);
logger.trace("Sent request to {}:{} with content = {}", DISCOVERY_MULTICAST_ADDRESS,
DISCOVERY_PORT, DISCOVER_REQUEST);
} catch (IOException e) {
logger.debug("Network error", e);
}
});

do {
try {
Expand All @@ -167,10 +190,10 @@ private class StatusReceiver extends Thread {
private boolean stopped = false;
private @Nullable DiscoveryResultReceiver discoveryResultReceiver;

private @Nullable MulticastSocket socket;
private @Nullable DatagramSocket socket;

StatusReceiver() {
super("GoveeStatusReceiver");
super("OH-binding-" + GoveeBindingConstants.BINDING_ID + "-StatusReceiver");
}

synchronized void setDiscoveryResultsReceiver(@Nullable DiscoveryResultReceiver receiver) {
Expand All @@ -195,13 +218,14 @@ void stopReceiving() {
public void run() {
while (!stopped) {
try {
socket = new MulticastSocket(RESPONSE_PORT);
DatagramSocket loopSocket = new DatagramSocket(RESPONSE_PORT);
this.socket = loopSocket;
byte[] buffer = new byte[10240];
socket.setReuseAddress(true);
loopSocket.setReuseAddress(true);
while (!stopped) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
if (!socket.isClosed()) {
socket.receive(packet);
if (!loopSocket.isClosed()) {
loopSocket.receive(packet);
} else {
logger.warn("Socket was unexpectedly closed");
break;
Expand All @@ -212,7 +236,7 @@ public void run() {

String response = new String(packet.getData(), packet.getOffset(), packet.getLength());
String deviceIPAddress = packet.getAddress().toString().replace("/", "");
logger.trace("Response from {} = {}", deviceIPAddress, response);
logger.trace("Received response from {} with content = {}", deviceIPAddress, response);

final DiscoveryResultReceiver discoveryReceiver;
synchronized (this) {
Expand Down Expand Up @@ -240,20 +264,22 @@ public void run() {
handler = thingHandlers.get(deviceIPAddress);
}
if (handler == null) {
logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress);
logger.warn("Handler not found for {}", deviceIPAddress);
} else {
logger.debug("processing status updates for thing {} ", handler.getThing().getLabel());
logger.debug("Processing response for {} on {}", handler.getThing().getUID(),
deviceIPAddress);
handler.handleIncomingStatus(response);
}
}
}
} catch (IOException e) {
logger.warn("exception when receiving status packet", e);
logger.debug("Exception when receiving status packet {}", e.getMessage());
// 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
// PS it also seems to be 'normal' to encounter errors when in device discovery mode
} finally {
if (socket != null) {
socket.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,4 +25,7 @@ public class GoveeConfiguration {

public String hostname = "";
public int refreshInterval = 5; // in seconds

public @Nullable Integer minKelvin;
public @Nullable Integer maxKelvin;
}
Loading