[govee] New Govee LAN-API Binding (#15696)

Signed-off-by: Stefan Höhn <mail@stefanhoehn.com>
This commit is contained in:
stefan-hoehn 2023-12-17 13:38:31 +01:00 committed by GitHub
parent 0c6a80a3be
commit 329f2b71e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1797 additions and 0 deletions

View File

@ -123,6 +123,7 @@
/bundles/org.openhab.binding.generacmobilelink/ @digitaldan
/bundles/org.openhab.binding.globalcache/ @mhilbush
/bundles/org.openhab.binding.goecharger/ @SamuelBrucksch
/bundles/org.openhab.binding.govee/ @stefan-hoehn
/bundles/org.openhab.binding.gpio/ @nils-bauer
/bundles/org.openhab.binding.gpstracker/ @gbicskei
/bundles/org.openhab.binding.gree/ @markus7017

View File

@ -606,6 +606,11 @@
<artifactId>org.openhab.binding.goecharger</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.govee</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.gpio</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,170 @@
# Govee Lan-API Binding
![govee](doc/govee-lights.png)
This binding integrates Light devices from [Govee](https://www.govee.com/).
Even though these devices are widely used, they are usually only accessable via the Cloud.
Another option is using Bluetooth which, due to its limitation only allows to control devices within a small range.
The Bluetooth approach is supported by the openHAB Govee Binding while this binding covers the LAN interface.
Fortunately, there is a [LAN API](https://app-h5.govee.com/user-manual/wlan-guide) that allows to control the devices within your own network without accessing the Cloud.
Note, though, that is somehow limited to a number of devices listed in the aforementioned manual.
The binding is aware of all the devices that are listed in that document and even provides a product description during discovery.
Note: By intent the Cloud API has not been implemented (so far) as it makes controlling Govee devices dependent by Govee service itself.
## Supported Things
The things that are supported are all lights.
While Govee provides probably more than a hundred different lights, only the following are supported officially by the LAN API, even though others might works as well.
Here is a list of the supported devices (the ones marked with * have been tested by the author)
- H619Z RGBIC Pro LED Strip Lights
- H6046 RGBIC TV Light Bars
- H6047 RGBIC Gaming Light Bars with Smart Controller
- H6061 Glide Hexa LED Panels (*)
- H6062 Glide Wall Light
- H6065 Glide RGBIC Y Lights
- H6066 Glide Hexa Pro LED Panel
- H6067 Glide Triangle Light Panels (*)
- H6072 RGBICWW Corner Floor Lamp
- H6076 RGBICW Smart Corner Floor Lamp (*)
- H6073 LED Floor Lamp
- H6078 Cylinder Floor Lamp
- 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
- 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
- H610B Music Wall Lights
- H6172 Outdoor LED Strip 10m
- H61B2 RGBIC Neon TV Backlight
- 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
- 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
- H7055 Pathway Light
- H7060 LED Flood Lights (2-Pack)
- H7061 LED Flood Lights (4-Pack)
- H7062 LED Flood Lights (6-Pack)
- 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
## Discovery
Discovery is done by scanning the devices in the Thing section.
The devices _do not_ support the LAN API support out-of-the-box.
To be able to use the device with the LAN API, the following needs to be done (also see the "Preparations for LAN API Control" section in the [Goveee LAN API Manual](https://app-h5.govee.com/user-manual/wlan-guide)):
- Start the Govee APP and add / discover the device (via Bluetooth) as described by the vendor manual
Go to the settings page of the device
![govee device settings](doc/device-settings.png)
- Note that it may take several(!) minutes until this setting comes up.
- Switch on the LAN Control setting.
- Now the device can be used with openHAB.
- The easiest way is then to scan the devices via the SCAN button in the thing section of that binding
## Thing Configuration
Even though binding configuration is supported via a thing file it should be noted that the IP address is required and there is no easy way to find out the IP address of the device.
One possibility is to look for the MAC address in the Govee app and then looking the IP address up via:
```shell
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 |
| deviceType | text | The product number of the device | N/A | yes | no |
| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
## Channels
| Channel | Type | Description | Read/Write | Description |
|-----------------------|--------|---------------------------------|------------|----------------------|
| color | Switch | On / Off | RW | Power On / OFF |
| | Color | HSB (Hue Saturation Brightness) | RW | |
| | Dimmer | Brightness Percentage | RW | |
| color-temperature | Dimmer | Color Temperature Percentage | RW | |
| color-temperature-abs | Dimmer | Color Temperature Absolute | RW | in 2000-9000 Kelvin |
Note: you may want to set Unit metadata to "K" when creating a color-temperature-abs item.
## UI Example for one device
![ui-example.png](doc/ui-example.png)
Thing channel setup:
![channel-setup1.png](doc/channel-setup1.png)
![channel-setup2.png](doc/channel-setup2.png)
![channel-setup3.png](doc/channel-setup3.png)
```java
UID: govee:govee-light:33_5F_60_74_F4_08_77_21
label: Govee H6159 RGB Light Strip H6159 (192.168.178.173)
thingTypeUID: govee:govee-light
configuration:
deviceType: H6159
wifiSoftwareVersion: 1.02.11
hostname: 192.168.162.233
macAddress: 33:5F:60:74:F4:08:66:21
wifiHardwareVersion: 1.00.10
refreshInterval: 5
productName: H6159 RGB Light Strip
channels:
- id: color
channelTypeUID: system:color
label: Color
description: Controls the color of the light
configuration: {}
- id: color-temperature
channelTypeUID: system:color-temperature
label: Color Temperature
description: Controls the color temperature of the light from 0 (cold) to 100 (warm)
configuration: {}
- id: color-temperature-abs
channelTypeUID: govee:color-temperature-abs
label: Absolute Color Temperature
description: Controls the color temperature of the light in Kelvin
configuration: {}
```
## Additional Information
Please provide any feedback regarding unlisted devices that even though not mentioned herein do work.

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.govee</artifactId>
<name>openHAB Add-ons :: Bundles :: Govee Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.govee-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-govee" description="Govee Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.govee/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,258 @@
/**
* Copyright (c) 2010-2023 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.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
/**
* The {@link CommunicationManager} is a thread that handles the answers of all devices.
* Therefore it needs to apply the information to the right thing.
*
* Discovery uses the same response code, so we must not refresh the status during discovery.
*
* @author Stefan Höhn - Initial contribution
* @author Danny Baumann - Thread-Safe design refactoring
*/
@NonNullByDefault
@Component(service = CommunicationManager.class)
public class CommunicationManager {
private final Gson gson = new Gson();
// Holds a list of all thing handlers to send them thing updates via the receiver-Thread
private final Map<String, GoveeHandler> thingHandlers = new HashMap<>();
@Nullable
private StatusReceiver receiverThread;
private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250";
private static final int DISCOVERY_PORT = 4001;
private static final int RESPONSE_PORT = 4002;
private static final int REQUEST_PORT = 4003;
private static final int INTERFACE_TIMEOUT_SEC = 5;
private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";
public interface DiscoveryResultReceiver {
void onResultReceived(DiscoveryResponse result);
}
@Activate
public CommunicationManager() {
}
public void registerHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.put(handler.getHostname(), handler);
if (receiverThread == null) {
receiverThread = new StatusReceiver();
receiverThread.start();
}
}
}
public void unregisterHandler(GoveeHandler handler) {
synchronized (thingHandlers) {
thingHandlers.remove(handler.getHostname());
if (thingHandlers.isEmpty()) {
StatusReceiver receiver = receiverThread;
if (receiver != null) {
receiver.stopReceiving();
}
receiverThread = null;
}
}
}
public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException {
final String hostname = handler.getHostname();
final DatagramSocket socket = new DatagramSocket();
socket.setReuseAddress(true);
final String message = gson.toJson(request);
final byte[] data = message.getBytes();
final InetAddress address = InetAddress.getByName(hostname);
DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
// logger.debug("Sending {} to {}", message, hostname);
socket.send(packet);
socket.close();
}
public void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultReceiver receiver) throws IOException {
synchronized (receiver) {
StatusReceiver localReceiver = null;
StatusReceiver activeReceiver = null;
try {
if (receiverThread == null) {
localReceiver = new StatusReceiver();
localReceiver.start();
activeReceiver = localReceiver;
} else {
activeReceiver = receiverThread;
}
if (activeReceiver != null) {
activeReceiver.setDiscoveryResultsReceiver(receiver);
}
final InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS);
final InetSocketAddress socketAddress = new InetSocketAddress(broadcastAddress, RESPONSE_PORT);
final Instant discoveryStartTime = Instant.now();
final Instant discoveryEndTime = discoveryStartTime.plusSeconds(INTERFACE_TIMEOUT_SEC);
try (MulticastSocket sendSocket = new MulticastSocket(socketAddress)) {
sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000);
sendSocket.setReuseAddress(true);
sendSocket.setBroadcast(true);
sendSocket.setTimeToLive(2);
sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, RESPONSE_PORT), intf);
byte[] requestData = DISCOVER_REQUEST.getBytes();
DatagramPacket request = new DatagramPacket(requestData, requestData.length, broadcastAddress,
DISCOVERY_PORT);
sendSocket.send(request);
}
do {
try {
receiver.wait(INTERFACE_TIMEOUT_SEC * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (Instant.now().isBefore(discoveryEndTime));
} finally {
if (activeReceiver != null) {
activeReceiver.setDiscoveryResultsReceiver(null);
}
if (localReceiver != null) {
localReceiver.stopReceiving();
}
}
}
}
private class StatusReceiver extends Thread {
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
private boolean stopped = false;
private @Nullable DiscoveryResultReceiver discoveryResultReceiver;
private @Nullable MulticastSocket socket;
StatusReceiver() {
super("GoveeStatusReceiver");
}
synchronized void setDiscoveryResultsReceiver(@Nullable DiscoveryResultReceiver receiver) {
discoveryResultReceiver = receiver;
}
void stopReceiving() {
stopped = true;
interrupt();
if (socket != null) {
socket.close();
}
try {
join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void run() {
while (!stopped) {
try {
socket = new MulticastSocket(RESPONSE_PORT);
byte[] buffer = new byte[10240];
socket.setReuseAddress(true);
while (!stopped) {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
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) {
// this probably was a status message
}
} 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;
}
}
}
}
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link GoveeBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class GoveeBindingConstants {
// Thing properties
public static final String MAC_ADDRESS = "macAddress";
public static final String IP_ADDRESS = "hostname";
public static final String DEVICE_TYPE = "deviceType";
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";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light");
// List of all Channel ids
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMPERATURE = "color-temperature";
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "color-temperature-abs";
// Limit values of channels
public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0;
public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.0;
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class GoveeConfiguration {
public String hostname = "";
public int refreshInterval = 5; // in seconds
}

View File

@ -0,0 +1,198 @@
/**
* Copyright (c) 2010-2023 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.io.IOException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.govee.internal.model.DiscoveryData;
import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovers Govee devices
*
* Scan approach:
* 1. Determines all local network interfaces
* 2. Send a multicast message on each interface to the Govee multicast address 239.255.255.250 at port 4001
* 3. Retrieve the list of devices
*
* Based on the description at https://app-h5.govee.com/user-manual/wlan-guide
*
* A typical scan response looks as follows
*
* <pre>{@code
* {
* "msg":{
* "cmd":"scan",
* "data":{
* "ip":"192.168.1.23",
* "device":"1F:80:C5:32:32:36:72:4E",
* "sku":"Hxxxx",
* "bleVersionHard":"3.01.01",
* "bleVersionSoft":"1.03.01",
* "wifiVersionHard":"1.00.10",
* "wifiVersionSoft":"1.02.03"
* }
* }
* }
* }
* </pre>
*
* Note that it uses the same port for receiving data like when receiving devices status updates.
*
* @see GoveeHandler
*
* @author Stefan Höhn - Initial Contribution
* @author Danny Baumann - Thread-Safe design refactoring
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee")
public class GoveeDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class);
private CommunicationManager communicationManager;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
@Activate
public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider,
@Reference CommunicationManager communicationManager) {
super(SUPPORTED_THING_TYPES_UIDS, 0, false);
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
this.communicationManager = communicationManager;
}
// for test purposes only
public GoveeDiscoveryService(CommunicationManager communicationManager) {
super(SUPPORTED_THING_TYPES_UIDS, 0, false);
this.communicationManager = communicationManager;
}
@Override
protected void startScan() {
logger.debug("starting Scan");
getLocalNetworkInterfaces().forEach(localNetworkInterface -> {
logger.debug("Discovering Govee devices on {} ...", localNetworkInterface);
try {
communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> {
DiscoveryResult result = responseToResult(response);
if (result != null) {
thingDiscovered(result);
}
});
logger.trace("After runDiscoveryForInterface");
} catch (IOException e) {
logger.debug("Discovery with IO exception: {}", e.getMessage());
}
logger.trace("After try");
});
}
public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
final DiscoveryData data = response.msg().data();
final String macAddress = data.device();
if (macAddress.isEmpty()) {
logger.warn("Empty Mac address received during discovery - ignoring {}", response);
return null;
}
final String ipAddress = data.ip();
if (ipAddress.isEmpty()) {
logger.warn("Empty IP address received during discovery - ignoring {}", response);
return null;
}
final String sku = data.sku();
if (sku.isEmpty()) {
logger.warn("Empty SKU (product name) received during discovery - ignoring {}", response);
return null;
}
final String productName;
if (i18nProvider != null) {
Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class);
productName = i18nProvider.getText(bundle, "discovery.govee-light." + sku, null,
localeProvider.getLocale());
} else {
productName = sku;
}
String nameForLabel = productName != null ? productName + " " + sku : sku;
ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_"));
DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUid)
.withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS)
.withProperty(GoveeBindingConstants.MAC_ADDRESS, macAddress)
.withProperty(GoveeBindingConstants.IP_ADDRESS, ipAddress)
.withProperty(GoveeBindingConstants.DEVICE_TYPE, sku)
.withLabel(String.format("Govee %s (%s)", nameForLabel, ipAddress));
if (productName != null) {
builder.withProperty(GoveeBindingConstants.PRODUCT_NAME, productName);
}
String hwVersion = data.wifiVersionHard();
if (hwVersion != null) {
builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
}
String swVersion = data.wifiVersionSoft();
if (swVersion != null) {
builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
}
return builder.build();
}
private List<NetworkInterface> getLocalNetworkInterfaces() {
List<NetworkInterface> result = new LinkedList<>();
try {
for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
try {
if (networkInterface.isUp() && !networkInterface.isLoopback()
&& !networkInterface.isPointToPoint()) {
result.add(networkInterface);
}
} catch (SocketException exception) {
// ignore
}
}
} catch (SocketException exception) {
return List.of();
}
return result;
}
}

View File

@ -0,0 +1,329 @@
/**
* Copyright (c) 2010-2023 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 static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
import java.io.IOException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.govee.internal.model.Color;
import org.openhab.binding.govee.internal.model.ColorData;
import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData;
import org.openhab.binding.govee.internal.model.GenericGoveeMsg;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.openhab.binding.govee.internal.model.StatusResponse;
import org.openhab.binding.govee.internal.model.ValueIntData;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.util.ColorUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link GoveeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* Any device has its own job that triggers a refresh of retrieving the external state from the device.
* However, there must be only one job that listens for all devices in a singleton thread because
* all devices send their udp packet response to the same port on openHAB. Based on the sender IP address
* of the device we can detect to which thing the status answer needs to be assigned to and updated.
*
* <ul>
* <li>The job per thing that triggers a new update is called <i>triggerStatusJob</i>. There are as many instances
* as things.</li>
* <li>The job that receives the answers and applies that to the respective thing is called <i>refreshStatusJob</i> and
* there is only one for all instances. It may be stopped and restarted by the DiscoveryService (see below).</li>
* </ul>
*
* The other topic that needs to be managed is that device discovery responses are also sent to openHAB at the same port
* as status updates. Therefore, when scanning new devices that job that listens to status devices must
* be stopped while scanning new devices. Otherwise, the status job will receive the scan discover UDB packages.
*
* Controlling the lights is done via the Govee LAN API (cloud is not supported):
* https://app-h5.govee.com/user-manual/wlan-guide
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class GoveeHandler extends BaseThingHandler {
/*
* Messages to be sent to the Govee devices
*/
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
@Nullable
private ScheduledFuture<?> triggerStatusJob; // send device status update job
private GoveeConfiguration goveeConfiguration = new GoveeConfiguration();
private CommunicationManager communicationManager;
private int lastOnOff;
private int lastBrightness;
private HSBType lastColor = new HSBType();
private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue();
/**
* This thing related job <i>thingRefreshSender</i> triggers an update to the Govee device.
* The device sends it back to the common port and the response is
* then received by the common #refreshStatusReceiver
*/
private final Runnable thingRefreshSender = () -> {
try {
triggerDeviceStatusRefresh();
if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.ONLINE);
}
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ "\"]");
}
};
public GoveeHandler(Thing thing, CommunicationManager communicationManager) {
super(thing);
this.communicationManager = communicationManager;
}
public String getHostname() {
return goveeConfiguration.hostname;
}
@Override
public void initialize() {
goveeConfiguration = getConfigAs(GoveeConfiguration.class);
final String ipAddress = goveeConfiguration.hostname;
if (ipAddress.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error.ip-address.missing");
return;
}
updateStatus(ThingStatus.UNKNOWN);
communicationManager.registerHandler(this);
if (triggerStatusJob == null) {
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100,
goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS);
}
}
@Override
public void dispose() {
super.dispose();
ScheduledFuture<?> triggerStatusJobFuture = triggerStatusJob;
if (triggerStatusJobFuture != null) {
triggerStatusJobFuture.cancel(true);
triggerStatusJob = null;
}
communicationManager.unregisterHandler(this);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
try {
if (command instanceof RefreshType) {
// we are refreshing all channels at once, as we get all information at the same time
triggerDeviceStatusRefresh();
logger.debug("Triggering Refresh");
} else {
logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass());
switch (channelUID.getId()) {
case CHANNEL_COLOR:
if (command instanceof HSBType hsbCommand) {
int[] rgb = ColorUtil.hsbToRgb(hsbCommand);
sendColor(new Color(rgb[0], rgb[1], rgb[2]));
} else if (command instanceof PercentType percent) {
sendBrightness(percent.intValue());
} else if (command instanceof OnOffType onOffCommand) {
sendOnOff(onOffCommand);
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof PercentType percent) {
logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", command);
Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.intValue()
* (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0);
lastColorTempInKelvin = colorTemp.intValue();
logger.debug("lastColorTempInKelvin {}", lastColorTempInKelvin);
sendColorTemp(lastColorTempInKelvin);
}
break;
case CHANNEL_COLOR_TEMPERATURE_ABS:
if (command instanceof QuantityType<?> quantity) {
logger.debug("Color Temperature Absolute change with Percent Type {}", command);
lastColorTempInKelvin = quantity.intValue();
logger.debug("COLOR_TEMPERATURE_ABS: lastColorTempInKelvin {}", lastColorTempInKelvin);
int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin
- COLOR_TEMPERATURE_MIN_VALUE)
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
logger.debug("computed lastColorTempInPercent {}", lastColorTempInPercent);
sendColorTemp(lastColorTempInKelvin);
}
break;
}
}
if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
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
*
*/
private void triggerDeviceStatusRefresh() throws IOException {
logger.debug("trigger Refresh Status of device {}", thing.getLabel());
GenericGoveeRequest lightQuery = new GenericGoveeRequest(
new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData()));
communicationManager.sendRequest(this, lightQuery);
}
public void sendColor(Color color) throws IOException {
lastColor = ColorUtil.rgbToHsb(new int[] { color.r(), color.g(), color.b() });
GenericGoveeRequest lightColor = new GenericGoveeRequest(
new GenericGoveeMsg("colorwc", new ColorData(color, 0)));
communicationManager.sendRequest(this, lightColor);
}
public void sendBrightness(int brightness) throws IOException {
lastBrightness = brightness;
GenericGoveeRequest lightBrightness = new GenericGoveeRequest(
new GenericGoveeMsg("brightness", new ValueIntData(brightness)));
communicationManager.sendRequest(this, lightBrightness);
}
private void sendOnOff(OnOffType switchValue) throws IOException {
lastOnOff = (switchValue == OnOffType.ON) ? 1 : 0;
GenericGoveeRequest switchLight = new GenericGoveeRequest(
new GenericGoveeMsg("turn", new ValueIntData(lastOnOff)));
communicationManager.sendRequest(this, switchLight);
}
private void sendColorTemp(int colorTemp) throws IOException {
lastColorTempInKelvin = colorTemp;
logger.debug("sendColorTemp {}", colorTemp);
GenericGoveeRequest lightColor = new GenericGoveeRequest(
new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp)));
communicationManager.sendRequest(this, lightColor);
}
/**
* Creates a Color state by using the last color information from lastColor
* The brightness is overwritten either by the provided lastBrightness
* or if lastOnOff = 0 (off) then the brightness is set 0
*
* @see #lastColor
* @see #lastBrightness
* @see #lastOnOff
*
* @return the computed state
*/
private HSBType getColorState(Color color, int brightness) {
PercentType computedBrightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(brightness);
int[] rgb = { color.r(), color.g(), color.b() };
HSBType hsb = ColorUtil.rgbToHsb(rgb);
return new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness);
}
void handleIncomingStatus(String response) {
if (response.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.empty-response");
return;
}
try {
StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class);
if (statusMessage != null) {
updateDeviceState(statusMessage);
}
updateStatus(ThingStatus.ONLINE);
} catch (JsonSyntaxException jse) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, jse.getMessage());
}
}
public void updateDeviceState(@Nullable StatusResponse message) {
if (message == null) {
return;
}
logger.trace("Receiving Device State");
int newOnOff = message.msg().data().onOff();
logger.trace("newOnOff = {}", newOnOff);
int newBrightness = message.msg().data().brightness();
logger.trace("newBrightness = {}", newBrightness);
Color newColor = message.msg().data().color();
logger.trace("newColor = {}", newColor);
int newColorTempInKelvin = message.msg().data().colorTemInKelvin();
logger.trace("newColorTempInKelvin = {}", newColorTempInKelvin);
newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE)
? COLOR_TEMPERATURE_MIN_VALUE.intValue()
: newColorTempInKelvin;
int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE)
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
HSBType adaptedColor = getColorState(newColor, newBrightness);
logger.trace("HSB old: {} vs adaptedColor: {}", lastColor, adaptedColor);
// avoid noise by only updating if the value has changed on the device
if (!adaptedColor.equals(lastColor)) {
logger.trace("UPDATING HSB old: {} != {}", lastColor, adaptedColor);
updateState(CHANNEL_COLOR, adaptedColor);
}
// avoid noise by only updating if the value has changed on the device
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
newColorTempInPercent, newColorTempInKelvin);
if (newColorTempInKelvin != lastColorTempInKelvin) {
logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
newColorTempInPercent, newColorTempInKelvin);
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType<>(lastColorTempInKelvin, Units.KELVIN));
updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(newColorTempInPercent));
}
lastOnOff = newOnOff;
lastColor = adaptedColor;
lastBrightness = newBrightness;
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2023 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 static org.openhab.binding.govee.internal.GoveeBindingConstants.THING_TYPE_LIGHT;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link GoveeHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.govee", service = ThingHandlerFactory.class)
public class GoveeHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT);
private CommunicationManager communicationManager;
@Activate
public GoveeHandlerFactory(@Reference CommunicationManager communicationManager) {
this.communicationManager = communicationManager;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_LIGHT.equals(thingTypeUID)) {
return new GoveeHandler(thing, communicationManager);
}
return null;
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.model;
/**
*
* @param r red
* @param g green
* @param b blue
*
* @author Stefan Höhn - Initial contribution
*/
public record Color(int r, int g, int b) {
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Color Data
*
* @param color
* @param colorTemInKelvin
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record ColorData(Color color, int colorTemInKelvin) implements GenericGoveeData {
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Govee Message - Device information
*
* @param ip IP address of the device
* @param device mac Address
* @param sku article number
* @param bleVersionHard Bluetooth HW version
* @param bleVersionSoft Bluetooth SW version
* @param wifiVersionHard Wifi HW version
* @param wifiVersionSoft Wife SW version
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record DiscoveryData(String ip, String device, String sku, String bleVersionHard, String bleVersionSoft,
String wifiVersionHard, String wifiVersionSoft) {
public DiscoveryData() {
this("", "", "", "", "", "", "");
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Govee Message
*
* @param cmd
* @param data
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record DiscoveryMsg(String cmd, DiscoveryData data) {
public DiscoveryMsg() {
this("", new DiscoveryData());
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Govee Message
*
* @param msg message block
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record DiscoveryResponse(DiscoveryMsg msg) {
public DiscoveryResponse() {
this(new DiscoveryMsg());
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Empty Govee Value Data
* Used to query device data
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record EmptyValueQueryStatusData() implements GenericGoveeData {
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Govee Data Interface
*
* can hold different type of data content
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public interface GenericGoveeData {
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Generic Govee Data
*
* can hold different types of data with the command
*
* @param cmd
* @param data
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record GenericGoveeMsg(String cmd, GenericGoveeData data) {
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Govee Message
*
* @param msg message block
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record GenericGoveeRequest(GenericGoveeMsg msg) {
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2023 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.model;
/**
*
* @param onOff on=1 off=0
* @param brightness brightness
* @param color rgb color
* @param colorTemInKelvin color temperature in Kelvin
*
* @author Stefan Höhn - Initial contribution
*/
public record StatusData(int onOff, int brightness, Color color, int colorTemInKelvin) {
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 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.model;
/**
* Govee Message - Cmd
*
* @param cmd Query Command
* @param data Status data
*
* @author Stefan Höhn - Initial contribution
*/
public record StatusMsg(String cmd, StatusData data) {
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2023 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.model;
/**
* Govee Message
*
* @param msg message block
*
* @author Stefan Höhn - Initial contribution
*/
public record StatusResponse(StatusMsg msg) {
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Simple Govee Value Data
* typically used for On / Off
*
* @param value
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record ValueIntData(int value) implements GenericGoveeData {
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2023 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.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Simple Govee Value Data
* typically used for On / Off
*
* @param value
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public record ValueStringData(String value) implements GenericGoveeData {
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="govee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>Govee Lan-API Binding</name>
<description>This is the binding for handling Govee Lights via the LAN-API interface.</description>
<connection>local</connection>
</addon:addon>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:govee:govee-light">
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname/IP Address</label>
<description>Hostname or IP address of the device</description>
</parameter>
<parameter name="macAddress" type="text" required="true">
<label>MAC Address</label>
<description>MAC Address of the device</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s">
<label>Light Refresh Interval</label>
<description>The amount of time that passes until the device is refreshed (in seconds)</description>
<default>2</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,80 @@
# add-on
addon.name = Govee Binding
addon.description = This is the binding for handling Govee Lights via the LAN-API interface.
# thing types
thing-type.govee-light.label = Govee Light Thing
thing-type.govee-light.description = Govee Light controllable via LAN API
# thing types config
thing-type.config.govee-light.refreshInterval.label = Light refresh interval (sec)
thing-type.config.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed
# product names
discovery.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights
discovery.govee-light.H6046 = H6046 RGBIC TV Light Bars
discovery.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller
discovery.govee-light.H6061 = H6061 Glide Hexa LED Panels
discovery.govee-light.H6062 = H6062 Glide Wall Light
discovery.govee-light.H6065 = H6065 Glide RGBIC Y Lights
discovery.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel
discovery.govee-light.H6067 = H6067 Glide Triangle Light Panels
discovery.govee-light.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.H6078 = H6078 Cylinder Floor Lamp
discovery.govee-light.H6087 = H6087 RGBIC Smart Wall Sconces
discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights
discovery.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M
discovery.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating
discovery.govee-light.H619C = H619C LED Strip Lights With Protective Coating
discovery.govee-light.H619D = H619D RGBIC PRO LED Strip Lights
discovery.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating
discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M
discovery.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M
discovery.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M
discovery.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light
discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10
discovery.govee-light.H61A8 = H61A8Neon 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.H610B = H610B Music Wall Lights
discovery.govee-light.H6172 = H6172 Outdoor LED Strip 10m
discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight
discovery.govee-light.H61E1 = H61E1 LED Strip Light M1
discovery.govee-light.H7012 = H7012 Warm White Outdoor String Lights
discovery.govee-light.H7013 = H7013 Warm White Outdoor String Lights
discovery.govee-light.H7021 = H7021 RGBIC Warm White Smart Outdoor String
discovery.govee-light.H7028 = H7028 Lynx Dream LED-Bulb String
discovery.govee-light.H7041 = H7041 LED Outdoor Bulb String Lights
discovery.govee-light.H7042 = H7042 LED Outdoor Bulb String Lights
discovery.govee-light.H705A = H705A Permanent Outdoor Lights 30M
discovery.govee-light.H705B = H705B Permanent Outdoor Lights 15M
discovery.govee-light.H7050 = H7050 Outdoor Ground Lights 11M
discovery.govee-light.H7051 = H7051 Outdoor Ground Lights 15M
discovery.govee-light.H7055 = H7055 Pathway Light
discovery.govee-light.H7060 = H7060 LED Flood Lights (2-Pack)
discovery.govee-light.H7061 = H7061 LED Flood Lights (4-Pack)
discovery.govee-light.H7062 = H7062 LED Flood Lights (6-Pack)
discovery.govee-light.H7065 = H7065 Outdoor Spot Lights
discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp
discovery.govee-light.H6056 = H6056 H6056 Flow Plus
discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids
discovery.govee-light.H618F = H618F RGBIC LED Strip Lights
discovery.govee-light.H618E = H618E LED Strip Lights 22m
discovery.govee-light.H6168 = H6168 TV LED Backlight
# thing status descriptions
offline.communication-error.could-not-query-device = Could not control/query device at IP address {0}
offline.configuration-error.ip-address.missing = IP address is missing
offline.communication-error.empty-response = Empty response received

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="govee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="govee-light">
<label>Govee Light</label>
<description>Govee light controllable via LAN API</description>
<channels>
<channel id="color" typeId="system.color"/>
<channel id="color-temperature" typeId="system.color-temperature"/>
<channel id="color-temperature-abs" typeId="color-temperature-abs"/>
</channels>
<config-description-ref uri="thing-type:govee:govee-light"/>
</thing-type>
<channel-type id="color-temperature-abs">
<item-type>Number:Temperature</item-type>
<label>Absolute Color Temperature </label>
<description>Controls the color temperature of the light in Kelvin</description>
<category>Temperature</category>
<tags>
<tag>Control</tag>
<tag>ColorTemperature</tag>
</tags>
<state min="2000" max="9000" pattern="%.0f K"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2023 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 static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.binding.govee.internal.model.DiscoveryResponse;
import org.openhab.core.config.discovery.DiscoveryResult;
import com.google.gson.Gson;
/**
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class GoveeDiscoveryTest {
String response = """
{
"msg":{
"cmd":"scan",
"data":{
"ip":"192.168.178.171",
"device":"7D:31:C3:35:33:33:44:15",
"sku":"H6076",
"bleVersionHard":"3.01.01",
"bleVersionSoft":"1.04.04",
"wifiVersionHard":"1.00.10",
"wifiVersionSoft":"1.02.11"
}
}
}
""";
@Test
public void testProcessScanMessage() {
GoveeDiscoveryService service = new GoveeDiscoveryService(new CommunicationManager());
DiscoveryResponse resp = new Gson().fromJson(response, DiscoveryResponse.class);
Objects.requireNonNull(resp);
@Nullable
DiscoveryResult result = service.responseToResult(resp);
assertNotNull(result);
Map<String, Object> deviceProperties = result.getProperties();
assertEquals(deviceProperties.get(GoveeBindingConstants.DEVICE_TYPE), "H6076");
assertEquals(deviceProperties.get(GoveeBindingConstants.IP_ADDRESS), "192.168.178.171");
assertEquals(deviceProperties.get(GoveeBindingConstants.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15");
}
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2023 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 static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.govee.internal.model.Color;
import org.openhab.binding.govee.internal.model.ColorData;
import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData;
import org.openhab.binding.govee.internal.model.GenericGoveeMsg;
import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
import org.openhab.binding.govee.internal.model.ValueIntData;
import com.google.gson.Gson;
/**
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class GoveeSerializeTest {
private static final Gson GSON = new Gson();
private final String lightOffJsonString = "{\"msg\":{\"cmd\":\"turn\",\"data\":{\"value\":0}}}";
private final String lightOnJsonString = "{\"msg\":{\"cmd\":\"brightness\",\"data\":{\"value\":100}}}";
private final String lightColorJsonString = "{\"msg\":{\"cmd\":\"colorwc\",\"data\":{\"color\":{\"r\":0,\"g\":1,\"b\":2},\"colorTemInKelvin\":3}}}";
private final String lightBrightnessJsonString = "{\"msg\":{\"cmd\":\"brightness\",\"data\":{\"value\":99}}}";
private final String lightQueryJsonString = "{\"msg\":{\"cmd\":\"devStatus\",\"data\":{}}}";
@Test
public void testSerializeMessage() {
GenericGoveeRequest lightOff = new GenericGoveeRequest(new GenericGoveeMsg("turn", new ValueIntData(0)));
assertEquals(lightOffJsonString, GSON.toJson(lightOff));
GenericGoveeRequest lightOn = new GenericGoveeRequest(new GenericGoveeMsg("brightness", new ValueIntData(100)));
assertEquals(lightOnJsonString, GSON.toJson(lightOn));
GenericGoveeRequest lightColor = new GenericGoveeRequest(
new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 1, 2), 3)));
assertEquals(lightColorJsonString, GSON.toJson(lightColor));
GenericGoveeRequest lightBrightness = new GenericGoveeRequest(
new GenericGoveeMsg("brightness", new ValueIntData(99)));
assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness));
GenericGoveeRequest lightQuery = new GenericGoveeRequest(
new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData()));
assertEquals(lightQueryJsonString, GSON.toJson(lightQuery));
}
}

View File

@ -154,6 +154,7 @@
<module>org.openhab.binding.gce</module>
<module>org.openhab.binding.generacmobilelink</module>
<module>org.openhab.binding.goecharger</module>
<module>org.openhab.binding.govee</module>
<module>org.openhab.binding.gpio</module>
<module>org.openhab.binding.globalcache</module>
<module>org.openhab.binding.gpstracker</module>