diff --git a/CODEOWNERS b/CODEOWNERS index 26ebb2c02cf2c..b3c666c0941c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -122,6 +122,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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 95eba8ac828c2..402dbf1be82c5 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -601,6 +601,11 @@ org.openhab.binding.goecharger ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.govee + ${project.version} + org.openhab.addons.bundles org.openhab.binding.gpio diff --git a/bundles/org.openhab.binding.govee/NOTICE b/bundles/org.openhab.binding.govee/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.govee/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md new file mode 100644 index 0000000000000..fca210a9b2cbf --- /dev/null +++ b/bundles/org.openhab.binding.govee/README.md @@ -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. diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup1.png b/bundles/org.openhab.binding.govee/doc/channel-setup1.png new file mode 100644 index 0000000000000..de86255dac23b Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup1.png differ diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup2.png b/bundles/org.openhab.binding.govee/doc/channel-setup2.png new file mode 100644 index 0000000000000..ab770d5502946 Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup2.png differ diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup3.png b/bundles/org.openhab.binding.govee/doc/channel-setup3.png new file mode 100644 index 0000000000000..52b4675793ae3 Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup3.png differ diff --git a/bundles/org.openhab.binding.govee/doc/device-settings.png b/bundles/org.openhab.binding.govee/doc/device-settings.png new file mode 100644 index 0000000000000..053096ce0abc5 Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/device-settings.png differ diff --git a/bundles/org.openhab.binding.govee/doc/govee-lights.png b/bundles/org.openhab.binding.govee/doc/govee-lights.png new file mode 100644 index 0000000000000..1917086dd66b7 Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/govee-lights.png differ diff --git a/bundles/org.openhab.binding.govee/doc/ui-example.png b/bundles/org.openhab.binding.govee/doc/ui-example.png new file mode 100644 index 0000000000000..fd23c6b0b6be4 Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/ui-example.png differ diff --git a/bundles/org.openhab.binding.govee/pom.xml b/bundles/org.openhab.binding.govee/pom.xml new file mode 100644 index 0000000000000..6a7245634acb3 --- /dev/null +++ b/bundles/org.openhab.binding.govee/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.1.0-SNAPSHOT + + + org.openhab.binding.govee + + openHAB Add-ons :: Bundles :: Govee Binding + + diff --git a/bundles/org.openhab.binding.govee/src/main/feature/feature.xml b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml new file mode 100644 index 0000000000000..8d0ae40c5d9fc --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.govee/${project.version} + + diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java new file mode 100644 index 0000000000000..c3931dcafa94c --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java @@ -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 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; + } + } + } + } + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java new file mode 100644 index 0000000000000..280a08adcce29 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java @@ -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; +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java new file mode 100644 index 0000000000000..319cb573e2539 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java @@ -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 +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java new file mode 100644 index 0000000000000..03aaee0e98a1c --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java @@ -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 + * + *
{@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"
+ *     }
+ *   }
+ * }
+ * }
+ * 
+ * + * 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 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 getLocalNetworkInterfaces() { + List 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; + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java new file mode 100644 index 0000000000000..1694541cad2d1 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java @@ -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. + * + *
    + *
  • The job per thing that triggers a new update is called triggerStatusJob. There are as many instances + * as things.
  • + *
  • The job that receives the answers and applies that to the respective thing is called refreshStatusJob and + * there is only one for all instances. It may be stopped and restarted by the DiscoveryService (see below).
  • + *
+ * + * 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 thingRefreshSender 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; + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java new file mode 100644 index 0000000000000..120610fc95b58 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java new file mode 100644 index 0000000000000..88c8dd4bbe659 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java new file mode 100644 index 0000000000000..c46fded8ab77a --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java @@ -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 { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java new file mode 100644 index 0000000000000..24d0411dfa47b --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java @@ -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("", "", "", "", "", "", ""); + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java new file mode 100644 index 0000000000000..f5bf218a4934f --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java @@ -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()); + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java new file mode 100644 index 0000000000000..a1b7ae5b8cec8 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java @@ -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()); + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java new file mode 100644 index 0000000000000..e04b4a0c174db --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java @@ -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 { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java new file mode 100644 index 0000000000000..bd8f6af1658cd --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java @@ -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 { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java new file mode 100644 index 0000000000000..89a5b31d5837d --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java new file mode 100644 index 0000000000000..f8bb47b945fee --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java new file mode 100644 index 0000000000000..33941a2e6b33c --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java new file mode 100644 index 0000000000000..8beb13388ee13 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java new file mode 100644 index 0000000000000..19286ec5033c9 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java new file mode 100644 index 0000000000000..81dcf25ef91ff --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java @@ -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 { +} diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java new file mode 100644 index 0000000000000..08ca5a019fc09 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java @@ -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 { +} diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..111e51af14da0 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,10 @@ + + + + binding + Govee Lan-API Binding + This is the binding for handling Govee Lights via the LAN-API interface. + local + diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..e153efa4d8067 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,24 @@ + + + + + + network-address + + Hostname or IP address of the device + + + + MAC Address of the device + + + + The amount of time that passes until the device is refreshed (in seconds) + 2 + + + + diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties new file mode 100644 index 0000000000000..b600839cb40cf --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties @@ -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 diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..002cb291f70c4 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,32 @@ + + + + + + Govee light controllable via LAN API + + + + + + + + + + + Number:Temperature + + Controls the color temperature of the light in Kelvin + Temperature + + Control + ColorTemperature + + + + + + diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java new file mode 100644 index 0000000000000..01c73d6e3c710 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java @@ -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 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"); + } +} diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java new file mode 100644 index 0000000000000..1aededf83e7f6 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java @@ -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)); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index b58688b52dbfa..22f5c77c84628 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -153,6 +153,7 @@ org.openhab.binding.gce org.openhab.binding.generacmobilelink org.openhab.binding.goecharger + org.openhab.binding.govee org.openhab.binding.gpio org.openhab.binding.globalcache org.openhab.binding.gpstracker