From af97edba12703199df5e0b02a9e07cffb26eae78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Mon, 1 May 2023 15:33:52 +0200 Subject: [PATCH 01/29] Nanoleaf codeowners change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 54efd66d255ad..2e4a2c55e3228 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -223,7 +223,7 @@ /bundles/org.openhab.binding.mynice/ @clinique /bundles/org.openhab.binding.myq/ @digitaldan /bundles/org.openhab.binding.mystrom/ @pail23 -/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn +/bundles/org.openhab.binding.nanoleaf/ @stefan-hoehn /bundles/org.openhab.binding.neato/ @jjlauterbach /bundles/org.openhab.binding.neeo/ @tmrobert8 /bundles/org.openhab.binding.neohub/ @andrewfg From 3f5c073f59c5a539e1caed8a614647b70a67e0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Wed, 4 Oct 2023 00:36:33 +0200 Subject: [PATCH 02/29] [govee lan api] initial contribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.goveelan/NOTICE | 13 + .../org.openhab.binding.goveelan/README.md | 130 +++++++ .../doc/device-settings.png | Bin 0 -> 10503 bytes .../doc/govee.png | Bin 0 -> 4580 bytes bundles/org.openhab.binding.goveelan/pom.xml | 25 ++ .../src/main/feature/feature.xml | 9 + .../internal/GoveeLanBindingConstants.java | 37 ++ .../internal/GoveeLanConfiguration.java | 29 ++ .../internal/GoveeLanDiscoveryService.java | 226 +++++++++++ .../goveelan/internal/GoveeLanHandler.java | 364 ++++++++++++++++++ .../internal/GoveeLanHandlerFactory.java | 54 +++ .../goveelan/internal/model/Color.java | 25 ++ .../internal/model/DiscoveryData.java | 31 ++ .../internal/model/DiscoveryMessage.java | 23 ++ .../goveelan/internal/model/DiscoveryMsg.java | 25 ++ .../goveelan/internal/model/StatusData.java | 26 ++ .../internal/model/StatusMessage.java | 23 ++ .../goveelan/internal/model/StatusMsg.java | 25 ++ .../src/main/resources/OH-INF/addon/addon.xml | 10 + .../main/resources/OH-INF/config/config.xml | 31 ++ .../resources/OH-INF/i18n/goveelan.properties | 79 ++++ .../resources/OH-INF/thing/thing-types.xml | 33 ++ .../internal/GoveeLanDiscoveryTest.java | 40 ++ 25 files changed, 1264 insertions(+) create mode 100644 bundles/org.openhab.binding.goveelan/NOTICE create mode 100644 bundles/org.openhab.binding.goveelan/README.md create mode 100644 bundles/org.openhab.binding.goveelan/doc/device-settings.png create mode 100644 bundles/org.openhab.binding.goveelan/doc/govee.png create mode 100644 bundles/org.openhab.binding.goveelan/pom.xml create mode 100644 bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties create mode 100644 bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 26ebb2c02cf2c..3e01c978bd055 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.goveelan/ @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..dfadb2d3f0e34 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.goveelan + ${project.version} + org.openhab.addons.bundles org.openhab.binding.gpio diff --git a/bundles/org.openhab.binding.goveelan/NOTICE b/bundles/org.openhab.binding.goveelan/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/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.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md new file mode 100644 index 0000000000000..1418356803c4d --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -0,0 +1,130 @@ +# Govee Lan-API Binding + +![govee](doc/govee.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 +- 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 + +## Binding Configuration + +There is no particular binding configuration needed. + +## 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 + +`arp -a | grep "MAC_ADDRESS"` + +### `sample` 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. | 3 | no | yes | + + +## Channels + +| Channel | Type | Read/Write | Description | +|---------------------|-------------------|------------|---------------------| +| switch | Switch | RW | Power On / OFF | +| color | Color HSB Type | RW | | +| colorTemperatureAbs | Color Temperature | RW | in 2000-9000 Kelvin | +| brightness | Percentage | RW | | + +Note: you may have to add "%.0f K" as the state description when creating a colorTemperatureAbs item. + +## Additional Information + +Please provide any feedback regarding unlisted devices that even though not mentioned herein do work. diff --git a/bundles/org.openhab.binding.goveelan/doc/device-settings.png b/bundles/org.openhab.binding.goveelan/doc/device-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..fa605fc0e1d006e2118b7785c0876b29d31dbb01 GIT binary patch literal 10503 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 O0000 + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.1.0-SNAPSHOT + + + + + com.google.code.gson + gson + 2.10.1 + + + + org.openhab.binding.goveelan + + openHAB Add-ons :: Bundles :: GoveeLan Binding + + diff --git a/bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml b/bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml new file mode 100644 index 0000000000000..094ece4302d1a --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/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.goveelan/${project.version} + + diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java new file mode 100644 index 0000000000000..1fb95afd781c8 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.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.goveelan.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link GoveeLanBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public class GoveeLanBindingConstants { + + private static final String BINDING_ID = "goveelan"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "goveeLight"); + + // List of all Channel ids + public static final String SWITCH = "switch"; + public static final String COLOR = "color"; + public static final String TEMPERATUR_ABS = "colorTemperatureAbs"; + public static final String BRIGHTNESS = "brightness"; +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java new file mode 100644 index 0000000000000..af604a2f1b2eb --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java @@ -0,0 +1,29 @@ +/** + * 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.goveelan.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link GoveeLanConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public class GoveeLanConfiguration { + public static final String IPADDRESS = "hostname"; + public static final String MAC_ADDRESS = "macaddress"; + public static final String DEVICETYPE = "devicetype"; + public static final String PRODUCTNAME = "productname"; + public int refreshInterval = 3; // in seconds +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java new file mode 100644 index 0000000000000..b0366977329e0 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java @@ -0,0 +1,226 @@ +/** + * 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.goveelan.internal; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.goveelan.internal.model.DiscoveryMessage; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +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.BundleContext; +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; + +import com.google.gson.Gson; + +/** + * 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 + * + * { + * "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 GoveeLanHandler + * + * @author Stefan Höhn - Initial Contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.goveelan") +public class GoveeLanDiscoveryService extends AbstractDiscoveryService { + + public static boolean discoveryActive = false; + + private final Logger logger = LoggerFactory.getLogger(GoveeLanDiscoveryService.class); + + private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250"; + private static final int DISCOVERY_PORT = 4001; + private static final int DISCOVERY_RESPONSE_PORT = 4002; + private static final int INTERFACE_TIMEOUT_SEC = 5; + private static final int MILLIS_PER_SEC = 1000; + + private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}"; + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set + .of(GoveeLanBindingConstants.THING_TYPE_LIGHT); + + @Activate + public GoveeLanDiscoveryService(@Reference TranslationProvider i18nProvider, + @Reference LocaleProvider localeProvider) throws IllegalArgumentException { + super(SUPPORTED_THING_TYPES_UIDS, 0, false); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + // for test purposes only + public GoveeLanDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, 0, false); + } + + @Override + protected void startScan() { + logger.debug("starting Scan"); + BundleContext bundleContext = FrameworkUtil.getBundle(GoveeLanDiscoveryService.class).getBundleContext(); + + try { + discoveryActive = true; + + // check if the status receiver is currently running, stop it and wait for that. + // note that it restarts itself as soon as we are done. + while (GoveeLanHandler.isRefreshJobRunning()) { + GoveeLanHandler.stopRefreshStatusJob(); + Thread.sleep(1000); + } + + InetAddress multicastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); + + getLocalNetworkInterfaces().forEach(localNetworkInterface -> { + logger.debug("Discovering Govee Devices on {} ...", localNetworkInterface); + try (MulticastSocket socket = new MulticastSocket(); + MulticastSocket rSocket = new MulticastSocket(DISCOVERY_RESPONSE_PORT)) { + + socket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); + rSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); + socket.setBroadcast(true); + socket.setTimeToLive(2); + + byte[] requestData = DISCOVER_REQUEST.getBytes(); + DatagramPacket request = new DatagramPacket(requestData, requestData.length, multicastAddress, + DISCOVERY_PORT); + socket.send(request); + + do { + byte[] rxbuf = new byte[10240]; + DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length); + rSocket.setReuseAddress(true); + rSocket.receive(packet); + + String response = new String(packet.getData()).trim(); + logger.trace("Govee Device Response: {}", response); + + Map properties = getDeviceProperties(response); + final String sku = properties.get(GoveeLanConfiguration.DEVICETYPE).toString(); + final String skuLabel = "discovery.goveelan.goveeLight." + sku; + String productName = i18nProvider.getText(bundleContext.getBundle(), skuLabel, sku, + Locale.getDefault()); + properties.put(GoveeLanConfiguration.PRODUCTNAME, (productName != null) ? productName : sku); + ThingUID thingUid = new ThingUID(GoveeLanBindingConstants.THING_TYPE_LIGHT, + properties.get(GoveeLanConfiguration.MAC_ADDRESS).toString().replace(":", "_")); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties) + .withRepresentationProperty(GoveeLanConfiguration.MAC_ADDRESS).withLabel( + "Govee " + productName + " " + properties.get(GoveeLanConfiguration.DEVICETYPE) + + " (" + properties.get(GoveeLanConfiguration.IPADDRESS) + ")"); + + thingDiscovered(discoveryResult.build()); + } while (true); // left by SocketTimeoutException + } catch (SocketTimeoutException ste) { + // done with scanning + } catch (IOException e) { + logger.warn("Discovery with IO exception: {}", e.getMessage()); + } + }); + } catch (UnknownHostException e) { + logger.warn("Discovery failed: {}", e.getMessage()); + } catch (InterruptedException e) { + } finally { + discoveryActive = false; + } + } + + public Map getDeviceProperties(String response) { + Gson gson = new Gson(); + + DiscoveryMessage message = gson.fromJson(response, DiscoveryMessage.class); + String ipAddress = message.msg().data().ip(); + String deviceType = message.msg().data().sku(); + String macAddress = message.msg().data().device(); + + Map properties = new HashMap<>(3); + properties.put(GoveeLanConfiguration.IPADDRESS, ipAddress); + properties.put(GoveeLanConfiguration.DEVICETYPE, deviceType); + properties.put(GoveeLanConfiguration.MAC_ADDRESS, macAddress); + + return properties; + } + + 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 Collections.emptyList(); + } + return result; + } + + public static boolean isDiscoveryActive() { + return discoveryActive; + } +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java new file mode 100644 index 0000000000000..ca47594382436 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -0,0 +1,364 @@ +/** + * 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.goveelan.internal; + +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.BRIGHTNESS; +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR; +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.SWITCH; +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.TEMPERATUR_ABS; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +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.goveelan.internal.model.Color; +import org.openhab.binding.goveelan.internal.model.StatusMessage; +import org.openhab.core.common.ThreadPoolManager; +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; + +/** + * The {@link GoveeLanHandler} 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 + * like 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. + * + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public class GoveeLanHandler extends BaseThingHandler { + + /* + * Messages to be sent to the Govee Devices + */ + private static final String LIGHT_OFF = "{\"msg\": {\"cmd\": \"turn\", \"data\": {\"value\": \"0\"}}}"; + // turning on via cmd-turn and value = 1 doesn't work, so let's use the brightness command + private static final String LIGHT_ON = """ + {"msg": {"cmd": "brightness", "data": {"value": "100"}}} + """; + private static final String LIGHT_COLOR = """ + { + "msg" : { + "cmd":"colorwc", + "data": { + "color" : { + "r" : %d, + "g" : %d, + "b": %d + }, + "colorTemInKelvin" : %d + } + } + } + """; + private static final String LIGHT_BRIGHTNESS = """ + { + "msg":{ + "cmd":"brightness", + "data":{ + "value": %d + } + } + } + """; + + private static final String QUERY_STATUS = """ + { + "msg":{ + "cmd":"devStatus", + "data":{ + } + } + } + """; + + // Holds a list of all thing handlers to send them thing updates via the receiver-Thread + private static final Map THING_HANDLERS = new HashMap<>(); + + private final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); + private static final int SENDTODEVICE_PORT = 4003; + private static final int RECEIVEFROMDEVICE_PORT = 4002; + + // Semaphores to suppress further processing if already running + private static boolean refreshJobRunning = false; + private static boolean refreshRunning = false; + + @Nullable + private static ScheduledFuture refreshStatusJob; // device response receiver job + @Nullable + private ScheduledFuture triggerStatusJob; // send device status update job + + /* + * Common Receiver job for the status answers of the devices + */ + public static boolean isRefreshJobRunning() { + return refreshJobRunning && THING_HANDLERS.isEmpty(); + } + + private static final Runnable REFRESH_STATUS_RECEIVER = () -> { + final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); + /** + * This thread receives an answer from any device. + * Therefore it needs to apply it to the right thing + */ + // Discovery uses the same response code, so we must not refresh the status during discovery + if (GoveeLanDiscoveryService.isDiscoveryActive()) { + LOGGER.debug("Not running refresh as Scan is currently active"); + } + + refreshJobRunning = true; + LOGGER.trace("REFRESH: running refresh cycle for {} devices", THING_HANDLERS.size()); + + if (THING_HANDLERS.isEmpty()) { + return; + } + + try (MulticastSocket socket = new MulticastSocket(RECEIVEFROMDEVICE_PORT)) { + byte[] buffer = new byte[10240]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + socket.setReuseAddress(true); + LOGGER.debug("waiting for Status"); + socket.receive(packet); + + String response = new String(packet.getData()).trim(); + String deviceIPAddress = packet.getAddress().toString().replace("/", ""); + LOGGER.trace("received = {} from {}", response, deviceIPAddress); + + GoveeLanHandler thingHandler = THING_HANDLERS.get(deviceIPAddress); + if (thingHandler == null) { + LOGGER.warn("thing Handler for {} couldn't be found.", deviceIPAddress); + return; + } + + LOGGER.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); + LOGGER.trace("Response from {} = {}", deviceIPAddress, response); + + Gson gson = new Gson(); + StatusMessage statusMessage = gson.fromJson(response, StatusMessage.class); + if (statusMessage != null) { + thingHandler.updateDeviceState(statusMessage); + } else { + LOGGER.warn("status message is null"); + } + } catch (IOException e) { + LOGGER.error("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); + } finally { + refreshJobRunning = false; + } + }; + + /** + * 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, + "Could not control/query device at IP address " + + thing.getProperties().get(GoveeLanConfiguration.IPADDRESS)); + } + }; + + public GoveeLanHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + GoveeLanConfiguration goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); + updateStatus(ThingStatus.ONLINE); + + String ipAddress = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + if (ipAddress != null) { + THING_HANDLERS.put(ipAddress, this); + } else { + LOGGER.warn("Handler for thing {} could not be added to list because ipaddress == null", thing.getLabel()); + } + + if (!THING_HANDLERS.isEmpty()) { + startRefreshStatusJob(); + } + + if (triggerStatusJob == null) { + LOGGER.debug("REFRESH: Starting refresh trigger job for thing {} ", thing.getLabel()); + triggerStatusJob = scheduler.scheduleAtFixedRate(thingRefreshSender, 100, + goveeLanConfiguration.refreshInterval * 1000, TimeUnit.MILLISECONDS); + } + } + + /** + * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) + */ + public static void stopRefreshStatusJob() { + refreshStatusJob.cancel(true); + refreshStatusJob = null; + refreshJobRunning = false; + } + + /** + * (re)start the refresh status job + */ + public static void startRefreshStatusJob() { + if (refreshStatusJob == null) { + refreshStatusJob = ThreadPoolManager.getScheduledPool("thingHandler") + .scheduleAtFixedRate(REFRESH_STATUS_RECEIVER, 100, 1000, TimeUnit.MILLISECONDS); + } + } + + @Override + public void dispose() { + super.dispose(); + + String ipAddress = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + triggerStatusJob.cancel(true); + triggerStatusJob = null; + THING_HANDLERS.remove(ipAddress); + if (THING_HANDLERS.isEmpty()) { + stopRefreshStatusJob(); + } + } + + @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(); + } else { + switch (channelUID.getId()) { + case SWITCH: + if (command instanceof OnOffType) { + send(command.equals(OnOffType.ON) ? LIGHT_ON : LIGHT_OFF); + } + break; + case COLOR: + if (command instanceof HSBType hsbCommand) { + int[] rgb = ColorUtil.hsbToRgb(hsbCommand); + send(String.format(GoveeLanHandler.LIGHT_COLOR, rgb[0], rgb[1], rgb[2], 0)); + } + break; + case TEMPERATUR_ABS: + if (command instanceof QuantityType quantity) { + send(String.format(GoveeLanHandler.LIGHT_COLOR, 0, 0, 0, quantity.longValue())); + } + break; + case BRIGHTNESS: + if (command instanceof PercentType percent) { + send(String.format(GoveeLanHandler.LIGHT_BRIGHTNESS, percent.intValue())); + } + break; + } + } + if (!thing.getStatus().equals(ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); + } + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Could not control/query device at IP address " + + thing.getProperties().get(GoveeLanConfiguration.IPADDRESS)); + } + } + + /** + * Initiate a refresh to our thing devicee + * + */ + private void triggerDeviceStatusRefresh() throws IOException { + if (refreshRunning) { + return; + } + if (GoveeLanDiscoveryService.isDiscoveryActive()) { + LOGGER.debug("Not triggering refresh as Scan is currently active"); + return; + } + refreshRunning = true; + + LOGGER.debug("trigger Refresh Status of device {}", thing.getLabel()); + + try { + send(QUERY_STATUS); + } finally { + refreshRunning = false; + } + } + + public void send(String message) throws IOException { + DatagramSocket socket; + socket = new DatagramSocket(); + socket.setReuseAddress(true); + byte[] data = message.getBytes(); + + final String hostname = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + LOGGER.trace("Sending {} to {}", message, hostname); + InetAddress address = InetAddress.getByName(hostname); + DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); + socket.send(packet); + socket.close(); + } + + public void updateDeviceState(StatusMessage message) { + int lastOnOff = message.msg().data().onOff(); + int lastBrightness = message.msg().data().brightness(); + Color lastColor = message.msg().data().color(); + int lastColorTemperature = message.msg().data().colorTemInKelvin(); + + updateState(SWITCH, OnOffType.from(lastOnOff == 1)); + updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); + updateState(TEMPERATUR_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); + updateState(BRIGHTNESS, new PercentType(lastBrightness)); + } +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java new file mode 100644 index 0000000000000..fec316705c59a --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java @@ -0,0 +1,54 @@ +/** + * 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.goveelan.internal; + +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.*; + +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.Component; + +/** + * The {@link GoveeLanHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.goveelan", service = ThingHandlerFactory.class) +public class GoveeLanHandlerFactory extends BaseThingHandlerFactory { + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT); + + @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 GoveeLanHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java new file mode 100644 index 0000000000000..a93ddb3ccd106 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java new file mode 100644 index 0000000000000..8ee9ba5e84a56 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.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.goveelan.internal.model; + +/** + * Govee Message - Device information + * + * @param ip IP Address of the device + * @param device Mac Address + * @param sku artice 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 + */ +public record DiscoveryData(String ip, String device, String sku, String bleVersionHard, String bleVersionSoft, + String wifiVersionHard, String wifiVersionSoft) { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java new file mode 100644 index 0000000000000..718ac0724a7c6 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java @@ -0,0 +1,23 @@ +/** + * 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.goveelan.internal.model; + +/** + * Govee Message + * @ param msg + * + * @author Stefan Höhn - Initial contribution + */ +public record DiscoveryMessage(DiscoveryMsg msg) { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java new file mode 100644 index 0000000000000..55c73188df4ad --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.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.goveelan.internal.model; + +/** + * Govee Message + * + * @param cmd + * @param data + * + * @author Stefan Höhn - Initial contribution + */ +public record DiscoveryMsg(String cmd, DiscoveryData data) { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java new file mode 100644 index 0000000000000..72909f62e72c4 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.internal.model; + +/** + * + * @param onOff on=1 off=0 + * @param brightness brightness + * @param color rgb Color + * @param colorTemInKelvin color 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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java new file mode 100644 index 0000000000000..1623a00bdebb6 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java @@ -0,0 +1,23 @@ +/** + * 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.goveelan.internal.model; + +/** + * Govee Message + * @ param msg + * + * @author Stefan Höhn - Initial contribution + */ +public record StatusMessage(StatusMsg msg) { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java new file mode 100644 index 0000000000000..8e5c21fbecaca --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.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.goveelan/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..8e8a09eccbafc --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/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. + + diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..5ab3a71358c33 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,31 @@ + + + + + + + Hostname or IP Address of the device + + + + MAC Address of the device + + + + The product number of the device + + + + Description of the device + + + + The amount of time that passes until the device is refreshed + 2 + + + + diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties new file mode 100644 index 0000000000000..60c7f4483d949 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties @@ -0,0 +1,79 @@ +# add-on + +addon.goveelan.name = GoveeLan Binding +addon.goveelan.description = This is the binding for handling Govee Lights via the LAN-API interface. + +# thing types + +thing-type.goveelan.goveeLight.label = Govee Light Thing +thing-type.goveelan.goveeLight.description = Govee Light controllable via LAN API + +# thing types config + +thing-type.config.goveelan.goveeLight.refreshInterval.label = Light refresh interval (sec) +thing-type.config.goveelan.goveeLight.refreshInterval.description = The amount of time that passes until the device is refreshed + +# channel types + +channel-type.goveelan.colortemperatureabs.label = Color Temperature (Absolute) +channel-type.goveelan.colortemperatureabs.description = Controls the color temperature of the light in Kelvin + +# product names + +discovery.goveelan.goveeLight.H619Z = H619Z RGBIC Pro LED Strip Lights +discovery.goveelan.goveeLight.H6046 = H6046 RGBIC TV Light Bars +discovery.goveelan.goveeLight.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller +discovery.goveelan.goveeLight.H6061 = H6061 Glide Hexa LED Panels +discovery.goveelan.goveeLight.H6062 = H6062 Glide Wall Light +discovery.goveelan.goveeLight.H6065 = H6065 Glide RGBIC Y Lights +discovery.goveelan.goveeLight.H6066 = H6066 Glide Hexa Pro LED Panel +discovery.goveelan.goveeLight.H6067 = H6067 Glide Triangle Light Panels +discovery.goveelan.goveeLight.H6072 = H6072 RGBICWW Corner Floor Lamp +discovery.goveelan.goveeLight.H6076 = H6076 RGBICW Smart Corner Floor Lamp +discovery.goveelan.goveeLight.H6073 = H6073 LED Floor Lamp +discovery.goveelan.goveeLight.H6078 = H6078 Cylinder Floor Lamp +discovery.goveelan.goveeLight.H6087 = H6087 RGBIC Smart Wall Sconces +discovery.goveelan.goveeLight.H6173 = H6173 RGBIC Outdoor Strip Lights +discovery.goveelan.goveeLight.H619A = H619A RGBIC Strip Lights With Protective Coating 5M +discovery.goveelan.goveeLight.H619B = H619B RGBIC LED Strip Lights With Protective Coating +discovery.goveelan.goveeLight.H619C = H619C LED Strip Lights With Protective Coating +discovery.goveelan.goveeLight.H619D = H619D RGBIC PRO LED Strip Lights +discovery.goveelan.goveeLight.H619E = H619E RGBIC LED Strip Lights With Protective Coating +discovery.goveelan.goveeLight.H61A0 = H61A0 RGBIC Neon Rope Light 1M +discovery.goveelan.goveeLight.H61A1 = H61A1 RGBIC Neon Rope Light 2M +discovery.goveelan.goveeLight.H61A2 = H61A2 RGBIC Neon Rope Light 5M +discovery.goveelan.goveeLight.H61A3 = H61A3 RGBIC Neon Rope Light +discovery.goveelan.goveeLight.H61A5 = H61A5 Neon LED Strip Light 10 +discovery.goveelan.goveeLight.H61A8 = H61A8Neon Neon Rope Light 10 +discovery.goveelan.goveeLight.H618A = H618A RGBIC Basic LED Strip Lights 5M +discovery.goveelan.goveeLight.H618C = H618C RGBIC Basic LED Strip Lights 5M +discovery.goveelan.goveeLight.H6117 = H6117 Dream Color LED Strip Light 10M +discovery.goveelan.goveeLight.H6159 = H6159 RGB Light Strip +discovery.goveelan.goveeLight.H615E = H615E LED Strip Lights 30M +discovery.goveelan.goveeLight.H6163 = H6163 Dreamcolor LED Strip Light 5M +discovery.goveelan.goveeLight.H610A = H610A Glide Lively Wall Lights +discovery.goveelan.goveeLight.H610B = H610B Music Wall Lights +discovery.goveelan.goveeLight.H6172 = H6172 Outdoor LED Strip 10m +discovery.goveelan.goveeLight.H61B2 = H61B2 RGBIC Neon TV Backlight +discovery.goveelan.goveeLight.H61E1 = H61E1 LED Strip Light M1 +discovery.goveelan.goveeLight.H7012 = H7012 Warm White Outdoor String Lights +discovery.goveelan.goveeLight.H7013 = H7013 Warm White Outdoor String Lights +discovery.goveelan.goveeLight.H7021 = H7021 RGBIC Warm White Smart Outdoor String +discovery.goveelan.goveeLight.H7028 = H7028 Lynx Dream LED-Bulb String +discovery.goveelan.goveeLight.H7041 = H7041 LED Outdoor Bulb String Lights +discovery.goveelan.goveeLight.H7042 = H7042 LED Outdoor Bulb String Lights +discovery.goveelan.goveeLight.H705A = H705A Permanent Outdoor Lights 30M +discovery.goveelan.goveeLight.H705B = H705B Permanent Outdoor Lights 15M +discovery.goveelan.goveeLight.H7050 = H7050 Outdoor Ground Lights 11M +discovery.goveelan.goveeLight.H7051 = H7051 Outdoor Ground Lights 15M +discovery.goveelan.goveeLight.H7055 = H7055 Pathway Light +discovery.goveelan.goveeLight.H7060 = H7060 LED Flood Lights (2-Pack) +discovery.goveelan.goveeLight.H7061 = H7061 LED Flood Lights (4-Pack) +discovery.goveelan.goveeLight.H7062 = H7062 LED Flood Lights (6-Pack) +discovery.goveelan.goveeLight.H7065 = H7065 Outdoor Spot Lights +discovery.goveelan.goveeLight.H6051 = H6051 Aura - Smart Table Lamp +discovery.goveelan.goveeLight.H6056 = H6056 H6056 Flow Plus +discovery.goveelan.goveeLight.H6059 = H6059 RGBWW Night Light for Kids +discovery.goveelan.goveeLight.H618F = H618F RGBIC LED Strip Lights +discovery.goveelan.goveeLight.H618E = H618E LED Strip Lights 22m +discovery.goveelan.goveeLight.H6168 = H6168 TV LED Backlight diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..39afc09705169 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,33 @@ + + + + + + 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.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java new file mode 100644 index 0000000000000..17482672d306d --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java @@ -0,0 +1,40 @@ +/** + * 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.goveelan.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public class GoveeLanDiscoveryTest { + + 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() { + GoveeLanDiscoveryService service = new GoveeLanDiscoveryService(); + Map deviceProperties = service.getDeviceProperties(response); + assertNotNull(deviceProperties); + assertEquals(deviceProperties.get(GoveeLanConfiguration.DEVICETYPE), "H6076"); + assertEquals(deviceProperties.get(GoveeLanConfiguration.IPADDRESS), "192.168.178.171"); + assertEquals(deviceProperties.get(GoveeLanConfiguration.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15"); + } +} From e5b2d52444474dc1ee0ea2d781a5c19549af1ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Wed, 4 Oct 2023 10:45:19 +0200 Subject: [PATCH 03/29] fix getting thing config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../binding/goveelan/internal/GoveeLanHandler.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index ca47594382436..4ee30b336a4cb 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -209,7 +209,7 @@ public static boolean isRefreshJobRunning() { } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Could not control/query device at IP address " - + thing.getProperties().get(GoveeLanConfiguration.IPADDRESS)); + + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS)); } }; @@ -222,7 +222,7 @@ public void initialize() { GoveeLanConfiguration goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); updateStatus(ThingStatus.ONLINE); - String ipAddress = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS).toString(); if (ipAddress != null) { THING_HANDLERS.put(ipAddress, this); } else { @@ -263,7 +263,7 @@ public static void startRefreshStatusJob() { public void dispose() { super.dispose(); - String ipAddress = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS).toString(); triggerStatusJob.cancel(true); triggerStatusJob = null; THING_HANDLERS.remove(ipAddress); @@ -309,7 +309,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Could not control/query device at IP address " - + thing.getProperties().get(GoveeLanConfiguration.IPADDRESS)); + + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS)); } } @@ -342,7 +342,8 @@ public void send(String message) throws IOException { socket.setReuseAddress(true); byte[] data = message.getBytes(); - final String hostname = thing.getProperties().get(GoveeLanConfiguration.IPADDRESS); + final String hostname = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS) + .toString(); LOGGER.trace("Sending {} to {}", message, hostname); InetAddress address = InetAddress.getByName(hostname); DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); From 012d27891b487d237ad1ef68f00656edf74b6a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Wed, 4 Oct 2023 22:24:03 +0200 Subject: [PATCH 04/29] add binding to main pom, remove gson from own pom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.goveelan/pom.xml | 8 -------- bundles/pom.xml | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/bundles/org.openhab.binding.goveelan/pom.xml b/bundles/org.openhab.binding.goveelan/pom.xml index 80d6525c19403..b5a57134cce44 100644 --- a/bundles/org.openhab.binding.goveelan/pom.xml +++ b/bundles/org.openhab.binding.goveelan/pom.xml @@ -10,14 +10,6 @@ 4.1.0-SNAPSHOT - - - com.google.code.gson - gson - 2.10.1 - - - org.openhab.binding.goveelan openHAB Add-ons :: Bundles :: GoveeLan Binding diff --git a/bundles/pom.xml b/bundles/pom.xml index b58688b52dbfa..fba1de4da41be 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.goveelan org.openhab.binding.gpio org.openhab.binding.globalcache org.openhab.binding.gpstracker From d9abaeeb5dcc2488ca03f44db46bba3198df0e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Fri, 6 Oct 2023 12:11:20 +0200 Subject: [PATCH 05/29] Lots of review comment fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../org.openhab.binding.goveelan/README.md | 28 ++-- .../doc/device-settings.png | Bin 10503 -> 25659 bytes .../doc/govee-lights.png | Bin 0 -> 106541 bytes .../doc/govee.png | Bin 4580 -> 0 bytes .../internal/GoveeLanBindingConstants.java | 4 +- .../internal/GoveeLanConfiguration.java | 10 +- .../internal/GoveeLanDiscoveryService.java | 15 +- .../goveelan/internal/GoveeLanHandler.java | 83 +++++++---- .../src/main/resources/OH-INF/addon/addon.xml | 2 +- .../main/resources/OH-INF/config/config.xml | 15 +- .../resources/OH-INF/i18n/goveelan.properties | 129 +++++++++--------- .../resources/OH-INF/thing/thing-types.xml | 12 +- .../internal/GoveeLanDiscoveryTest.java | 4 +- 13 files changed, 166 insertions(+), 136 deletions(-) create mode 100644 bundles/org.openhab.binding.goveelan/doc/govee-lights.png delete mode 100644 bundles/org.openhab.binding.goveelan/doc/govee.png diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md index 1418356803c4d..257ac28fdaca9 100644 --- a/bundles/org.openhab.binding.goveelan/README.md +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -1,6 +1,6 @@ # Govee Lan-API Binding -![govee](doc/govee.png) +![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. @@ -85,32 +85,30 @@ 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 ++ 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 - -## Binding Configuration - -There is no particular binding configuration needed. ++ 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 -`arp -a | grep "MAC_ADDRESS"` +``` +arp -a | grep "MAC_ADDRESS" +``` -### `sample` Thing Configuration +### 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 | +| 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. | 3 | no | yes | diff --git a/bundles/org.openhab.binding.goveelan/doc/device-settings.png b/bundles/org.openhab.binding.goveelan/doc/device-settings.png index fa605fc0e1d006e2118b7785c0876b29d31dbb01..053096ce0abc5f23d9c097bbc68454d0613c101f 100644 GIT binary patch literal 25659 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
f)b$}a9$lQd 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 z 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

PoF!`K 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�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

`?4t%J@nAGYe@cR%iB_b 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 Z-k%U(IaGzmXDz_RXJTk>P@`ub`F|59>0ST; literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.goveelan/doc/govee.png b/bundles/org.openhab.binding.goveelan/doc/govee.png deleted file mode 100644 index a2282763a7e07fab521bbda01bd0a5c7c5d7c8bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4580 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 O0000 properties = getDeviceProperties(response); - final String sku = properties.get(GoveeLanConfiguration.DEVICETYPE).toString(); - final String skuLabel = "discovery.goveelan.goveeLight." + sku; + final String sku = properties.get(GoveeLanConfiguration.DEVICE_TYPE).toString(); + final String skuLabel = "discovery.goveelan.govee-light." + sku; String productName = i18nProvider.getText(bundleContext.getBundle(), skuLabel, sku, Locale.getDefault()); - properties.put(GoveeLanConfiguration.PRODUCTNAME, (productName != null) ? productName : sku); + properties.put(GoveeLanConfiguration.PRODUCT_NAME, (productName != null) ? productName : sku); ThingUID thingUid = new ThingUID(GoveeLanBindingConstants.THING_TYPE_LIGHT, properties.get(GoveeLanConfiguration.MAC_ADDRESS).toString().replace(":", "_")); DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) .withProperties(properties) .withRepresentationProperty(GoveeLanConfiguration.MAC_ADDRESS).withLabel( - "Govee " + productName + " " + properties.get(GoveeLanConfiguration.DEVICETYPE) - + " (" + properties.get(GoveeLanConfiguration.IPADDRESS) + ")"); + "Govee " + productName + " " + properties.get(GoveeLanConfiguration.DEVICE_TYPE) + + " (" + properties.get(GoveeLanConfiguration.IP_ADDRESS) + ")"); thingDiscovered(discoveryResult.build()); } while (true); // left by SocketTimeoutException @@ -194,8 +193,8 @@ public Map getDeviceProperties(String response) { String macAddress = message.msg().data().device(); Map properties = new HashMap<>(3); - properties.put(GoveeLanConfiguration.IPADDRESS, ipAddress); - properties.put(GoveeLanConfiguration.DEVICETYPE, deviceType); + properties.put(GoveeLanConfiguration.IP_ADDRESS, ipAddress); + properties.put(GoveeLanConfiguration.DEVICE_TYPE, deviceType); properties.put(GoveeLanConfiguration.MAC_ADDRESS, macAddress); return properties; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index 4ee30b336a4cb..75792a9cf731e 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -14,8 +14,8 @@ import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.BRIGHTNESS; import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR; +import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR_TEMPERATURE_ABS; import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.SWITCH; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.TEMPERATUR_ABS; import java.io.IOException; import java.net.DatagramPacket; @@ -50,6 +50,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * The {@link GoveeLanHandler} is responsible for handling commands, which are @@ -143,13 +144,15 @@ public static boolean isRefreshJobRunning() { return refreshJobRunning && THING_HANDLERS.isEmpty(); } + public static final Gson GSON = new Gson(); private static final Runnable REFRESH_STATUS_RECEIVER = () -> { final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); - /** + /* * This thread receives an answer from any device. * Therefore it needs to apply it to the right thing + * + * Discovery uses the same response code, so we must not refresh the status during discovery */ - // Discovery uses the same response code, so we must not refresh the status during discovery if (GoveeLanDiscoveryService.isDiscoveryActive()) { LOGGER.debug("Not running refresh as Scan is currently active"); } @@ -161,6 +164,7 @@ public static boolean isRefreshJobRunning() { return; } + GoveeLanHandler thingHandler = null; try (MulticastSocket socket = new MulticastSocket(RECEIVEFROMDEVICE_PORT)) { byte[] buffer = new byte[10240]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); @@ -172,7 +176,7 @@ public static boolean isRefreshJobRunning() { String deviceIPAddress = packet.getAddress().toString().replace("/", ""); LOGGER.trace("received = {} from {}", response, deviceIPAddress); - GoveeLanHandler thingHandler = THING_HANDLERS.get(deviceIPAddress); + thingHandler = THING_HANDLERS.get(deviceIPAddress); if (thingHandler == null) { LOGGER.warn("thing Handler for {} couldn't be found.", deviceIPAddress); return; @@ -181,15 +185,27 @@ public static boolean isRefreshJobRunning() { LOGGER.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); LOGGER.trace("Response from {} = {}", deviceIPAddress, response); - Gson gson = new Gson(); - StatusMessage statusMessage = gson.fromJson(response, StatusMessage.class); - if (statusMessage != null) { - thingHandler.updateDeviceState(statusMessage); + if (response != null && !response.isEmpty()) { + try { + StatusMessage statusMessage = GSON.fromJson(response, StatusMessage.class); + thingHandler.updateDeviceState(statusMessage); + } catch (JsonSyntaxException jse) { + thingHandler.updateStatus(ThingStatus.OFFLINE); + } } else { - LOGGER.warn("status message is null"); + thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/error.offline.ip-address.emptyResponse"); + } + if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { + thingHandler.updateStatus(ThingStatus.ONLINE); } } catch (IOException e) { LOGGER.error("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); + // 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 { refreshJobRunning = false; } @@ -208,8 +224,8 @@ public static boolean isRefreshJobRunning() { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not control/query device at IP address " - + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS)); + "@text/error.couldNotQueryDevice [\"" + + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) + "\"]"); } }; @@ -220,31 +236,35 @@ public GoveeLanHandler(Thing thing) { @Override public void initialize() { GoveeLanConfiguration goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); - updateStatus(ThingStatus.ONLINE); - String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS).toString(); - if (ipAddress != null) { - THING_HANDLERS.put(ipAddress, this); + final Object ipAdress = getThing().getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS); + if (ipAdress != null) { + THING_HANDLERS.put(ipAdress.toString(), this); } else { - LOGGER.warn("Handler for thing {} could not be added to list because ipaddress == null", thing.getLabel()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/error.offline.ip-address.missing"); } - if (!THING_HANDLERS.isEmpty()) { startRefreshStatusJob(); } if (triggerStatusJob == null) { - LOGGER.debug("REFRESH: Starting refresh trigger job for thing {} ", thing.getLabel()); - triggerStatusJob = scheduler.scheduleAtFixedRate(thingRefreshSender, 100, - goveeLanConfiguration.refreshInterval * 1000, TimeUnit.MILLISECONDS); + LOGGER.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); + + triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100, + goveeLanConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS); } + + updateStatus(ThingStatus.UNKNOWN); } /** * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) */ public static void stopRefreshStatusJob() { - refreshStatusJob.cancel(true); + if (refreshStatusJob != null) { + refreshStatusJob.cancel(true); + } refreshStatusJob = null; refreshJobRunning = false; } @@ -255,7 +275,7 @@ public static void stopRefreshStatusJob() { public static void startRefreshStatusJob() { if (refreshStatusJob == null) { refreshStatusJob = ThreadPoolManager.getScheduledPool("thingHandler") - .scheduleAtFixedRate(REFRESH_STATUS_RECEIVER, 100, 1000, TimeUnit.MILLISECONDS); + .scheduleWithFixedDelay(REFRESH_STATUS_RECEIVER, 100, 1000, TimeUnit.MILLISECONDS); } } @@ -263,7 +283,8 @@ public static void startRefreshStatusJob() { public void dispose() { super.dispose(); - String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS).toString(); + String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS).toString(); + assert triggerStatusJob != null; triggerStatusJob.cancel(true); triggerStatusJob = null; THING_HANDLERS.remove(ipAddress); @@ -291,7 +312,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { send(String.format(GoveeLanHandler.LIGHT_COLOR, rgb[0], rgb[1], rgb[2], 0)); } break; - case TEMPERATUR_ABS: + case COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { send(String.format(GoveeLanHandler.LIGHT_COLOR, 0, 0, 0, quantity.longValue())); } @@ -308,8 +329,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not control/query device at IP address " - + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS)); + "@text/error.couldNotQueryDevice [\"" + + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) + "\"]"); } } @@ -342,7 +363,7 @@ public void send(String message) throws IOException { socket.setReuseAddress(true); byte[] data = message.getBytes(); - final String hostname = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IPADDRESS) + final String hostname = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) .toString(); LOGGER.trace("Sending {} to {}", message, hostname); InetAddress address = InetAddress.getByName(hostname); @@ -351,7 +372,11 @@ public void send(String message) throws IOException { socket.close(); } - public void updateDeviceState(StatusMessage message) { + public void updateDeviceState(@Nullable StatusMessage message) { + if (message == null) { + return; + } + int lastOnOff = message.msg().data().onOff(); int lastBrightness = message.msg().data().brightness(); Color lastColor = message.msg().data().color(); @@ -359,7 +384,7 @@ public void updateDeviceState(StatusMessage message) { updateState(SWITCH, OnOffType.from(lastOnOff == 1)); updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); - updateState(TEMPERATUR_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); + updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); updateState(BRIGHTNESS, new PercentType(lastBrightness)); } } diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml index 8e8a09eccbafc..4d7d1cb3ec097 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml @@ -6,5 +6,5 @@ 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.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml index 5ab3a71358c33..52fb37ba1de84 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -4,26 +4,27 @@ 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"> - + + network-address Hostname or IP Address of the device - + MAC Address of the device - + The product number of the device - + Description of the device - - - The amount of time that passes until the device is refreshed + + + The amount of time that passes until the device is refreshed (in seconds) 2 diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties index 60c7f4483d949..698a6b0dbaadc 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties @@ -5,13 +5,13 @@ addon.goveelan.description = This is the binding for handling Govee Lights via t # thing types -thing-type.goveelan.goveeLight.label = Govee Light Thing -thing-type.goveelan.goveeLight.description = Govee Light controllable via LAN API +thing-type.goveelan.govee-light.label = Govee Light Thing +thing-type.goveelan.govee-light.description = Govee Light controllable via LAN API # thing types config -thing-type.config.goveelan.goveeLight.refreshInterval.label = Light refresh interval (sec) -thing-type.config.goveelan.goveeLight.refreshInterval.description = The amount of time that passes until the device is refreshed +thing-type.config.goveelan.govee-light.refreshInterval.label = Light refresh interval (sec) +thing-type.config.goveelan.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed # channel types @@ -20,60 +20,67 @@ channel-type.goveelan.colortemperatureabs.description = Controls the color tempe # product names -discovery.goveelan.goveeLight.H619Z = H619Z RGBIC Pro LED Strip Lights -discovery.goveelan.goveeLight.H6046 = H6046 RGBIC TV Light Bars -discovery.goveelan.goveeLight.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller -discovery.goveelan.goveeLight.H6061 = H6061 Glide Hexa LED Panels -discovery.goveelan.goveeLight.H6062 = H6062 Glide Wall Light -discovery.goveelan.goveeLight.H6065 = H6065 Glide RGBIC Y Lights -discovery.goveelan.goveeLight.H6066 = H6066 Glide Hexa Pro LED Panel -discovery.goveelan.goveeLight.H6067 = H6067 Glide Triangle Light Panels -discovery.goveelan.goveeLight.H6072 = H6072 RGBICWW Corner Floor Lamp -discovery.goveelan.goveeLight.H6076 = H6076 RGBICW Smart Corner Floor Lamp -discovery.goveelan.goveeLight.H6073 = H6073 LED Floor Lamp -discovery.goveelan.goveeLight.H6078 = H6078 Cylinder Floor Lamp -discovery.goveelan.goveeLight.H6087 = H6087 RGBIC Smart Wall Sconces -discovery.goveelan.goveeLight.H6173 = H6173 RGBIC Outdoor Strip Lights -discovery.goveelan.goveeLight.H619A = H619A RGBIC Strip Lights With Protective Coating 5M -discovery.goveelan.goveeLight.H619B = H619B RGBIC LED Strip Lights With Protective Coating -discovery.goveelan.goveeLight.H619C = H619C LED Strip Lights With Protective Coating -discovery.goveelan.goveeLight.H619D = H619D RGBIC PRO LED Strip Lights -discovery.goveelan.goveeLight.H619E = H619E RGBIC LED Strip Lights With Protective Coating -discovery.goveelan.goveeLight.H61A0 = H61A0 RGBIC Neon Rope Light 1M -discovery.goveelan.goveeLight.H61A1 = H61A1 RGBIC Neon Rope Light 2M -discovery.goveelan.goveeLight.H61A2 = H61A2 RGBIC Neon Rope Light 5M -discovery.goveelan.goveeLight.H61A3 = H61A3 RGBIC Neon Rope Light -discovery.goveelan.goveeLight.H61A5 = H61A5 Neon LED Strip Light 10 -discovery.goveelan.goveeLight.H61A8 = H61A8Neon Neon Rope Light 10 -discovery.goveelan.goveeLight.H618A = H618A RGBIC Basic LED Strip Lights 5M -discovery.goveelan.goveeLight.H618C = H618C RGBIC Basic LED Strip Lights 5M -discovery.goveelan.goveeLight.H6117 = H6117 Dream Color LED Strip Light 10M -discovery.goveelan.goveeLight.H6159 = H6159 RGB Light Strip -discovery.goveelan.goveeLight.H615E = H615E LED Strip Lights 30M -discovery.goveelan.goveeLight.H6163 = H6163 Dreamcolor LED Strip Light 5M -discovery.goveelan.goveeLight.H610A = H610A Glide Lively Wall Lights -discovery.goveelan.goveeLight.H610B = H610B Music Wall Lights -discovery.goveelan.goveeLight.H6172 = H6172 Outdoor LED Strip 10m -discovery.goveelan.goveeLight.H61B2 = H61B2 RGBIC Neon TV Backlight -discovery.goveelan.goveeLight.H61E1 = H61E1 LED Strip Light M1 -discovery.goveelan.goveeLight.H7012 = H7012 Warm White Outdoor String Lights -discovery.goveelan.goveeLight.H7013 = H7013 Warm White Outdoor String Lights -discovery.goveelan.goveeLight.H7021 = H7021 RGBIC Warm White Smart Outdoor String -discovery.goveelan.goveeLight.H7028 = H7028 Lynx Dream LED-Bulb String -discovery.goveelan.goveeLight.H7041 = H7041 LED Outdoor Bulb String Lights -discovery.goveelan.goveeLight.H7042 = H7042 LED Outdoor Bulb String Lights -discovery.goveelan.goveeLight.H705A = H705A Permanent Outdoor Lights 30M -discovery.goveelan.goveeLight.H705B = H705B Permanent Outdoor Lights 15M -discovery.goveelan.goveeLight.H7050 = H7050 Outdoor Ground Lights 11M -discovery.goveelan.goveeLight.H7051 = H7051 Outdoor Ground Lights 15M -discovery.goveelan.goveeLight.H7055 = H7055 Pathway Light -discovery.goveelan.goveeLight.H7060 = H7060 LED Flood Lights (2-Pack) -discovery.goveelan.goveeLight.H7061 = H7061 LED Flood Lights (4-Pack) -discovery.goveelan.goveeLight.H7062 = H7062 LED Flood Lights (6-Pack) -discovery.goveelan.goveeLight.H7065 = H7065 Outdoor Spot Lights -discovery.goveelan.goveeLight.H6051 = H6051 Aura - Smart Table Lamp -discovery.goveelan.goveeLight.H6056 = H6056 H6056 Flow Plus -discovery.goveelan.goveeLight.H6059 = H6059 RGBWW Night Light for Kids -discovery.goveelan.goveeLight.H618F = H618F RGBIC LED Strip Lights -discovery.goveelan.goveeLight.H618E = H618E LED Strip Lights 22m -discovery.goveelan.goveeLight.H6168 = H6168 TV LED Backlight +discovery.goveelan.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights +discovery.goveelan.govee-light.H6046 = H6046 RGBIC TV Light Bars +discovery.goveelan.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller +discovery.goveelan.govee-light.H6061 = H6061 Glide Hexa LED Panels +discovery.goveelan.govee-light.H6062 = H6062 Glide Wall Light +discovery.goveelan.govee-light.H6065 = H6065 Glide RGBIC Y Lights +discovery.goveelan.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel +discovery.goveelan.govee-light.H6067 = H6067 Glide Triangle Light Panels +discovery.goveelan.govee-light.H6072 = H6072 RGBICWW Corner Floor Lamp +discovery.goveelan.govee-light.H6076 = H6076 RGBICW Smart Corner Floor Lamp +discovery.goveelan.govee-light.H6073 = H6073 LED Floor Lamp +discovery.goveelan.govee-light.H6078 = H6078 Cylinder Floor Lamp +discovery.goveelan.govee-light.H6087 = H6087 RGBIC Smart Wall Sconces +discovery.goveelan.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights +discovery.goveelan.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M +discovery.goveelan.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating +discovery.goveelan.govee-light.H619C = H619C LED Strip Lights With Protective Coating +discovery.goveelan.govee-light.H619D = H619D RGBIC PRO LED Strip Lights +discovery.goveelan.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating +discovery.goveelan.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M +discovery.goveelan.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M +discovery.goveelan.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M +discovery.goveelan.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light +discovery.goveelan.govee-light.H61A5 = H61A5 Neon LED Strip Light 10 +discovery.goveelan.govee-light.H61A8 = H61A8Neon Neon Rope Light 10 +discovery.goveelan.govee-light.H618A = H618A RGBIC Basic LED Strip Lights 5M +discovery.goveelan.govee-light.H618C = H618C RGBIC Basic LED Strip Lights 5M +discovery.goveelan.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M +discovery.goveelan.govee-light.H6159 = H6159 RGB Light Strip +discovery.goveelan.govee-light.H615E = H615E LED Strip Lights 30M +discovery.goveelan.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M +discovery.goveelan.govee-light.H610A = H610A Glide Lively Wall Lights +discovery.goveelan.govee-light.H610B = H610B Music Wall Lights +discovery.goveelan.govee-light.H6172 = H6172 Outdoor LED Strip 10m +discovery.goveelan.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight +discovery.goveelan.govee-light.H61E1 = H61E1 LED Strip Light M1 +discovery.goveelan.govee-light.H7012 = H7012 Warm White Outdoor String Lights +discovery.goveelan.govee-light.H7013 = H7013 Warm White Outdoor String Lights +discovery.goveelan.govee-light.H7021 = H7021 RGBIC Warm White Smart Outdoor String +discovery.goveelan.govee-light.H7028 = H7028 Lynx Dream LED-Bulb String +discovery.goveelan.govee-light.H7041 = H7041 LED Outdoor Bulb String Lights +discovery.goveelan.govee-light.H7042 = H7042 LED Outdoor Bulb String Lights +discovery.goveelan.govee-light.H705A = H705A Permanent Outdoor Lights 30M +discovery.goveelan.govee-light.H705B = H705B Permanent Outdoor Lights 15M +discovery.goveelan.govee-light.H7050 = H7050 Outdoor Ground Lights 11M +discovery.goveelan.govee-light.H7051 = H7051 Outdoor Ground Lights 15M +discovery.goveelan.govee-light.H7055 = H7055 Pathway Light +discovery.goveelan.govee-light.H7060 = H7060 LED Flood Lights (2-Pack) +discovery.goveelan.govee-light.H7061 = H7061 LED Flood Lights (4-Pack) +discovery.goveelan.govee-light.H7062 = H7062 LED Flood Lights (6-Pack) +discovery.goveelan.govee-light.H7065 = H7065 Outdoor Spot Lights +discovery.goveelan.govee-light.H6051 = H6051 Aura - Smart Table Lamp +discovery.goveelan.govee-light.H6056 = H6056 H6056 Flow Plus +discovery.goveelan.govee-light.H6059 = H6059 RGBWW Night Light for Kids +discovery.goveelan.govee-light.H618F = H618F RGBIC LED Strip Lights +discovery.goveelan.govee-light.H618E = H618E LED Strip Lights 22m +discovery.goveelan.govee-light.H6168 = H6168 TV LED Backlight + +# errors + +error.couldNotQueryDevice = Could not control/query device at IP address {0} +error.offline.ip-address.missing = Handler for thing could not be added to list because ipaddress == null +error.offline.ip-address.emptyResponse = Offline due to receiving an empty response +error.offline.ip-address.wrongResponse = Offline due to receiving an unexpected response diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml index 39afc09705169..b403a2c6d81e4 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -4,22 +4,22 @@ 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"> - - - Govee Light controllable via LAN API + + + Govee light controllable via LAN API - + - + Number:Temperature - + Controls the color temperature of the light in Kelvin Temperature diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java index 17482672d306d..73d96165a71b9 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java @@ -33,8 +33,8 @@ public void testProcessScanMessage() { GoveeLanDiscoveryService service = new GoveeLanDiscoveryService(); Map deviceProperties = service.getDeviceProperties(response); assertNotNull(deviceProperties); - assertEquals(deviceProperties.get(GoveeLanConfiguration.DEVICETYPE), "H6076"); - assertEquals(deviceProperties.get(GoveeLanConfiguration.IPADDRESS), "192.168.178.171"); + assertEquals(deviceProperties.get(GoveeLanConfiguration.DEVICE_TYPE), "H6076"); + assertEquals(deviceProperties.get(GoveeLanConfiguration.IP_ADDRESS), "192.168.178.171"); assertEquals(deviceProperties.get(GoveeLanConfiguration.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15"); } } From b8b4119476629edb05c18e766cfba247c40cff9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 7 Oct 2023 15:13:43 +0200 Subject: [PATCH 06/29] more review changes and refactorings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../org.openhab.binding.goveelan/README.md | 8 +- .../internal/GoveeLanBindingConstants.java | 5 + .../internal/GoveeLanConfiguration.java | 8 +- .../internal/GoveeLanDiscoveryService.java | 148 +++++++++++------- .../goveelan/internal/GoveeLanHandler.java | 88 +++++------ .../goveelan/internal/model/ColorData.java | 28 ++++ .../internal/model/DiscoveryData.java | 6 + .../internal/model/DiscoveryMessage.java | 6 + .../goveelan/internal/model/DiscoveryMsg.java | 6 + .../internal/model/GenericGoveeData.java | 27 ++++ .../internal/model/GenericGoveeMessage.java | 26 +++ .../internal/model/GenericGoveeMsg.java | 30 ++++ .../goveelan/internal/model/ValueData.java | 28 ++++ .../main/resources/OH-INF/config/config.xml | 2 +- .../resources/OH-INF/i18n/goveelan.properties | 7 +- .../resources/OH-INF/thing/thing-types.xml | 2 +- .../internal/GoveeLanDiscoveryTest.java | 6 +- .../internal/GoveeLanSerializeTest.java | 52 ++++++ 18 files changed, 362 insertions(+), 121 deletions(-) create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java create mode 100644 bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md index 257ac28fdaca9..d65992031a1e2 100644 --- a/bundles/org.openhab.binding.goveelan/README.md +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -88,9 +88,9 @@ To be able to use the device with the LAN API, the following needs to be done (a + 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 ++ 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 @@ -102,8 +102,6 @@ One possibility is to look for the MAC address in the Govee app and then looking arp -a | grep "MAC_ADDRESS" ``` -### Thing Configuration - | Name | Type | Description | Default | Required | Advanced | |-----------------|---------|---------------------------------------|---------|----------|----------| | hostname | text | Hostname or IP address of the device | N/A | yes | no | diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java index ba4434a9b7190..7ed4450c74cf7 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java @@ -24,6 +24,11 @@ @NonNullByDefault public class GoveeLanBindingConstants { + // Thing properties + public static final String IP_ADDRESS = "hostname"; + public static final String MAC_ADDRESS = "macAddress"; + public static final String DEVICE_TYPE = "deviceType"; + public static final String PRODUCT_NAME = "productName"; private static final String BINDING_ID = "goveelan"; // List of all Thing Type UIDs diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java index 35ca622636abf..ba7bed250a546 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java @@ -15,15 +15,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link GoveeLanConfiguration} class contains fields mapping thing configuration parameters. + * The {@link GoveeLanConfiguration} contains thing values that are used by the Thing Handler * * @author Stefan Höhn - Initial contribution */ @NonNullByDefault public class GoveeLanConfiguration { - public static final String IP_ADDRESS = "hostname"; - public static final String MAC_ADDRESS = "macAddress"; - public static final String DEVICE_TYPE = "deviceType"; - public static final String PRODUCT_NAME = "productName"; + + public String hostname = ""; public int refreshInterval = 2; // in seconds } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java index 22d391d4d0f0d..f104eb3ddc5c9 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java @@ -16,11 +16,11 @@ import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.NetworkInterface; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.net.UnknownHostException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -34,11 +34,10 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService; 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.BundleContext; +import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -101,11 +100,9 @@ public class GoveeLanDiscoveryService extends AbstractDiscoveryService { .of(GoveeLanBindingConstants.THING_TYPE_LIGHT); @Activate - public GoveeLanDiscoveryService(@Reference TranslationProvider i18nProvider, - @Reference LocaleProvider localeProvider) throws IllegalArgumentException { + public GoveeLanDiscoveryService(@Reference TranslationProvider i18nProvider) throws IllegalArgumentException { super(SUPPORTED_THING_TYPES_UIDS, 0, false); this.i18nProvider = i18nProvider; - this.localeProvider = localeProvider; } // for test purposes only @@ -116,7 +113,6 @@ public GoveeLanDiscoveryService() { @Override protected void startScan() { logger.debug("starting Scan"); - BundleContext bundleContext = FrameworkUtil.getBundle(GoveeLanDiscoveryService.class).getBundleContext(); try { discoveryActive = true; @@ -126,76 +122,114 @@ protected void startScan() { while (GoveeLanHandler.isRefreshJobRunning()) { GoveeLanHandler.stopRefreshStatusJob(); Thread.sleep(1000); + logger.debug("Waiting for device status request finish its task to be able to start discovery"); } - InetAddress multicastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); - getLocalNetworkInterfaces().forEach(localNetworkInterface -> { logger.debug("Discovering Govee Devices on {} ...", localNetworkInterface); - try (MulticastSocket socket = new MulticastSocket(); - MulticastSocket rSocket = new MulticastSocket(DISCOVERY_RESPONSE_PORT)) { - socket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); - rSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); - socket.setBroadcast(true); - socket.setTimeToLive(2); - - byte[] requestData = DISCOVER_REQUEST.getBytes(); - DatagramPacket request = new DatagramPacket(requestData, requestData.length, multicastAddress, - DISCOVERY_PORT); - socket.send(request); - - do { - byte[] rxbuf = new byte[10240]; - DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length); - rSocket.setReuseAddress(true); - rSocket.receive(packet); - - String response = new String(packet.getData()).trim(); - logger.trace("Govee Device Response: {}", response); - - Map properties = getDeviceProperties(response); - final String sku = properties.get(GoveeLanConfiguration.DEVICE_TYPE).toString(); - final String skuLabel = "discovery.goveelan.govee-light." + sku; - String productName = i18nProvider.getText(bundleContext.getBundle(), skuLabel, sku, - Locale.getDefault()); - properties.put(GoveeLanConfiguration.PRODUCT_NAME, (productName != null) ? productName : sku); - ThingUID thingUid = new ThingUID(GoveeLanBindingConstants.THING_TYPE_LIGHT, - properties.get(GoveeLanConfiguration.MAC_ADDRESS).toString().replace(":", "_")); - - DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) - .withProperties(properties) - .withRepresentationProperty(GoveeLanConfiguration.MAC_ADDRESS).withLabel( - "Govee " + productName + " " + properties.get(GoveeLanConfiguration.DEVICE_TYPE) - + " (" + properties.get(GoveeLanConfiguration.IP_ADDRESS) + ")"); - - thingDiscovered(discoveryResult.build()); - } while (true); // left by SocketTimeoutException - } catch (SocketTimeoutException ste) { - // done with scanning + try (MulticastSocket sendSocket = new MulticastSocket(); + MulticastSocket receiveSocket = new MulticastSocket(DISCOVERY_RESPONSE_PORT)) { + sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); + sendSocket.setBroadcast(true); + sendSocket.setTimeToLive(2); + InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); + sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, DISCOVERY_RESPONSE_PORT), + localNetworkInterface); + + sendBroadcastToDiscoverThing(sendSocket, receiveSocket, broadcastAddress); } catch (IOException e) { logger.warn("Discovery with IO exception: {}", e.getMessage()); } }); - } catch (UnknownHostException e) { - logger.warn("Discovery failed: {}", e.getMessage()); } catch (InterruptedException e) { + // don't care } finally { discoveryActive = false; } } + private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastSocket receiveSocket, + InetAddress broadcastAddress) throws IOException { + byte[] requestData = DISCOVER_REQUEST.getBytes(); + + DatagramPacket request = new DatagramPacket(requestData, requestData.length, broadcastAddress, DISCOVERY_PORT); + + try { + sendSocket.send(request); + } catch (SocketTimeoutException ste) { + // done with scanning + } + + receiveSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); + receiveSocket.setReuseAddress(true); + do { + byte[] rxbuf = new byte[10240]; + DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length); + receiveSocket.receive(packet); + + String response = new String(packet.getData()).trim(); + logger.trace("Govee Device Response: {}", response); + + final Map properties = getDeviceProperties(response); + final Object product = properties.get(GoveeLanBindingConstants.PRODUCT_NAME); + final String productName = (product != null) ? product.toString() : "unknown"; + final Object mac = properties.get(GoveeLanBindingConstants.MAC_ADDRESS); + final String macAddress = (mac != null) ? mac.toString() : "unknown"; + + ThingUID thingUid = new ThingUID(GoveeLanBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_")); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid).withProperties(properties) + .withRepresentationProperty(GoveeLanBindingConstants.MAC_ADDRESS) + .withLabel("Govee " + productName + " " + properties.get(GoveeLanBindingConstants.DEVICE_TYPE) + + " (" + properties.get(GoveeLanBindingConstants.IP_ADDRESS) + ")"); + + thingDiscovered(discoveryResult.build()); + } while (true); // left by SocketTimeoutException + } + public Map getDeviceProperties(String response) { + Bundle bundle = FrameworkUtil.getBundle(GoveeLanDiscoveryService.class); Gson gson = new Gson(); DiscoveryMessage message = gson.fromJson(response, DiscoveryMessage.class); - String ipAddress = message.msg().data().ip(); - String deviceType = message.msg().data().sku(); - String macAddress = message.msg().data().device(); + String ipAddress = ""; + String sku = ""; + String macAddress = ""; + String productName = ""; + + if (message != null) { + ipAddress = message.msg().data().ip(); + sku = message.msg().data().sku(); + macAddress = message.msg().data().device(); + + if (ipAddress.isEmpty()) { + ipAddress = "unknown"; + logger.warn("Empty IP Address received during discovery - device may not work"); + } + + productName = "unknown"; + if (!sku.isEmpty()) { + final String skuLabel = "discovery.goveelan.govee-light." + sku; + if (bundle != null) { + productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); + } + } else { + sku = "unknown"; + productName = "unknown"; + logger.warn("Empty SKU (product name) received during discovery - device may not work"); + } + + if (macAddress.isEmpty()) { + macAddress = "unknown"; + logger.warn("Empty Mac Address received during discovery - device may not work"); + } + } Map properties = new HashMap<>(3); - properties.put(GoveeLanConfiguration.IP_ADDRESS, ipAddress); - properties.put(GoveeLanConfiguration.DEVICE_TYPE, deviceType); - properties.put(GoveeLanConfiguration.MAC_ADDRESS, macAddress); + properties.put(GoveeLanBindingConstants.IP_ADDRESS, ipAddress); + properties.put(GoveeLanBindingConstants.DEVICE_TYPE, sku); + properties.put(GoveeLanBindingConstants.MAC_ADDRESS, macAddress); + properties.put(GoveeLanBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); return properties; } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index 75792a9cf731e..15462c5a84a89 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -31,7 +31,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.goveelan.internal.model.Color; +import org.openhab.binding.goveelan.internal.model.ColorData; +import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; +import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; import org.openhab.binding.goveelan.internal.model.StatusMessage; +import org.openhab.binding.goveelan.internal.model.ValueData; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; @@ -80,26 +84,15 @@ public class GoveeLanHandler extends BaseThingHandler { /* * Messages to be sent to the Govee Devices */ - private static final String LIGHT_OFF = "{\"msg\": {\"cmd\": \"turn\", \"data\": {\"value\": \"0\"}}}"; + private static final Gson GSON = new Gson(); + + private static final String LIGHT_OFF = GSON + .toJson(new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueData(0)))); + // turning on via cmd-turn and value = 1 doesn't work, so let's use the brightness command - private static final String LIGHT_ON = """ - {"msg": {"cmd": "brightness", "data": {"value": "100"}}} - """; - private static final String LIGHT_COLOR = """ - { - "msg" : { - "cmd":"colorwc", - "data": { - "color" : { - "r" : %d, - "g" : %d, - "b": %d - }, - "colorTemInKelvin" : %d - } - } - } - """; + private static final String LIGHT_ON = GSON + .toJson(new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueData(100)))); + private static final String LIGHT_BRIGHTNESS = """ { "msg":{ @@ -136,6 +129,7 @@ public class GoveeLanHandler extends BaseThingHandler { private static ScheduledFuture refreshStatusJob; // device response receiver job @Nullable private ScheduledFuture triggerStatusJob; // send device status update job + private GoveeLanConfiguration goveeLanConfiguration = new GoveeLanConfiguration(); /* * Common Receiver job for the status answers of the devices @@ -144,7 +138,6 @@ public static boolean isRefreshJobRunning() { return refreshJobRunning && THING_HANDLERS.isEmpty(); } - public static final Gson GSON = new Gson(); private static final Runnable REFRESH_STATUS_RECEIVER = () -> { final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); /* @@ -164,7 +157,7 @@ public static boolean isRefreshJobRunning() { return; } - GoveeLanHandler thingHandler = null; + GoveeLanHandler thingHandler; try (MulticastSocket socket = new MulticastSocket(RECEIVEFROMDEVICE_PORT)) { byte[] buffer = new byte[10240]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); @@ -185,16 +178,17 @@ public static boolean isRefreshJobRunning() { LOGGER.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); LOGGER.trace("Response from {} = {}", deviceIPAddress, response); - if (response != null && !response.isEmpty()) { + if (!response.isEmpty()) { try { StatusMessage statusMessage = GSON.fromJson(response, StatusMessage.class); thingHandler.updateDeviceState(statusMessage); } catch (JsonSyntaxException jse) { - thingHandler.updateStatus(ThingStatus.OFFLINE); + thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + jse.getMessage()); } } else { - thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/error.offline.ip-address.emptyResponse"); + thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/error.offline.communication-error.empty-response"); } if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { thingHandler.updateStatus(ThingStatus.ONLINE); @@ -224,8 +218,7 @@ public static boolean isRefreshJobRunning() { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.couldNotQueryDevice [\"" - + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) + "\"]"); + "@text/error.could-not-query-device [\"" + goveeLanConfiguration.hostname + "\"]"); } }; @@ -235,14 +228,15 @@ public GoveeLanHandler(Thing thing) { @Override public void initialize() { - GoveeLanConfiguration goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); + goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); - final Object ipAdress = getThing().getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS); - if (ipAdress != null) { - THING_HANDLERS.put(ipAdress.toString(), this); + final String ipAddress = goveeLanConfiguration.hostname; + if (ipAddress.isEmpty()) { + THING_HANDLERS.put(ipAddress, this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.offline.ip-address.missing"); + return; } if (!THING_HANDLERS.isEmpty()) { startRefreshStatusJob(); @@ -274,7 +268,7 @@ public static void stopRefreshStatusJob() { */ public static void startRefreshStatusJob() { if (refreshStatusJob == null) { - refreshStatusJob = ThreadPoolManager.getScheduledPool("thingHandler") + refreshStatusJob = ThreadPoolManager.getScheduledPool("goveeThingHandler") .scheduleWithFixedDelay(REFRESH_STATUS_RECEIVER, 100, 1000, TimeUnit.MILLISECONDS); } } @@ -283,11 +277,14 @@ public static void startRefreshStatusJob() { public void dispose() { super.dispose(); - String ipAddress = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS).toString(); - assert triggerStatusJob != null; - triggerStatusJob.cancel(true); - triggerStatusJob = null; - THING_HANDLERS.remove(ipAddress); + if (triggerStatusJob != null) { + triggerStatusJob.cancel(true); + triggerStatusJob = null; + } + if (!goveeLanConfiguration.hostname.isEmpty()) { + THING_HANDLERS.remove(goveeLanConfiguration.hostname); + } + if (THING_HANDLERS.isEmpty()) { stopRefreshStatusJob(); } @@ -309,12 +306,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { case COLOR: if (command instanceof HSBType hsbCommand) { int[] rgb = ColorUtil.hsbToRgb(hsbCommand); - send(String.format(GoveeLanHandler.LIGHT_COLOR, rgb[0], rgb[1], rgb[2], 0)); + GenericGoveeMsg lightColor = new GenericGoveeMsg("colorwc", + new ColorData(new Color(rgb[0], rgb[1], rgb[2]), 0)); + send(GSON.toJson(lightColor)); } break; case COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { - send(String.format(GoveeLanHandler.LIGHT_COLOR, 0, 0, 0, quantity.longValue())); + GenericGoveeMsg lightColor = new GenericGoveeMsg("colorwc", + new ColorData(new Color(0, 0, 0), quantity.intValue())); + send(GSON.toJson(lightColor)); } break; case BRIGHTNESS: @@ -329,8 +330,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.couldNotQueryDevice [\"" - + thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) + "\"]"); + "@text/error.could-not-query-device [\"" + goveeLanConfiguration.hostname + "\"]"); } } @@ -363,10 +363,8 @@ public void send(String message) throws IOException { socket.setReuseAddress(true); byte[] data = message.getBytes(); - final String hostname = thing.getConfiguration().getProperties().get(GoveeLanConfiguration.IP_ADDRESS) - .toString(); - LOGGER.trace("Sending {} to {}", message, hostname); - InetAddress address = InetAddress.getByName(hostname); + InetAddress address = InetAddress.getByName(goveeLanConfiguration.hostname); + LOGGER.trace("Sending {} to {}", message, goveeLanConfiguration.hostname); DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); socket.send(packet); socket.close(); diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java new file mode 100644 index 0000000000000..05fd2ac6d6609 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java index 8ee9ba5e84a56..92e3057ae5c02 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java @@ -13,6 +13,8 @@ */ package org.openhab.binding.goveelan.internal.model; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Govee Message - Device information * @@ -26,6 +28,10 @@ * * @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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java index 718ac0724a7c6..f757894f5ecda 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java @@ -13,11 +13,17 @@ */ package org.openhab.binding.goveelan.internal.model; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Govee Message * @ param msg * * @author Stefan Höhn - Initial contribution */ +@NonNullByDefault public record DiscoveryMessage(DiscoveryMsg msg) { + public DiscoveryMessage() { + this(new DiscoveryMsg()); + } } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java index 55c73188df4ad..878cfadebdafb 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java @@ -13,6 +13,8 @@ */ package org.openhab.binding.goveelan.internal.model; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Govee Message * @@ -21,5 +23,9 @@ * * @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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java new file mode 100644 index 0000000000000..bd7a6fd433d57 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java new file mode 100644 index 0000000000000..a1c587628dc6d --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.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.goveelan.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Govee Message + * @ param msg + * + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public record GenericGoveeMessage(GenericGoveeMsg msg) { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java new file mode 100644 index 0000000000000..eb9f0ada7fcb1 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/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.goveelan.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.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java new file mode 100644 index 0000000000000..62857b7f7382e --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.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.goveelan.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 ValueData(int value) implements GenericGoveeData { +} diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml index 52fb37ba1de84..8b66a07f8dfaa 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -4,7 +4,7 @@ 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"> - + network-address diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties index 698a6b0dbaadc..808492c09b826 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties @@ -78,9 +78,8 @@ discovery.goveelan.govee-light.H618F = H618F RGBIC LED Strip Lights discovery.goveelan.govee-light.H618E = H618E LED Strip Lights 22m discovery.goveelan.govee-light.H6168 = H6168 TV LED Backlight -# errors +# thing status descriptions -error.couldNotQueryDevice = Could not control/query device at IP address {0} +error.could-not-query-device = Could not control/query device at IP address {0} error.offline.ip-address.missing = Handler for thing could not be added to list because ipaddress == null -error.offline.ip-address.emptyResponse = Offline due to receiving an empty response -error.offline.ip-address.wrongResponse = Offline due to receiving an unexpected response +error.offline.communication-error.empty-response = Offline due to receiving an empty response diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml index b403a2c6d81e4..24b5a300b1128 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,7 +14,7 @@ - + diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java index 73d96165a71b9..77799596fd67a 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java @@ -33,8 +33,8 @@ public void testProcessScanMessage() { GoveeLanDiscoveryService service = new GoveeLanDiscoveryService(); Map deviceProperties = service.getDeviceProperties(response); assertNotNull(deviceProperties); - assertEquals(deviceProperties.get(GoveeLanConfiguration.DEVICE_TYPE), "H6076"); - assertEquals(deviceProperties.get(GoveeLanConfiguration.IP_ADDRESS), "192.168.178.171"); - assertEquals(deviceProperties.get(GoveeLanConfiguration.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15"); + assertEquals(deviceProperties.get(GoveeLanBindingConstants.DEVICE_TYPE), "H6076"); + assertEquals(deviceProperties.get(GoveeLanBindingConstants.IP_ADDRESS), "192.168.178.171"); + assertEquals(deviceProperties.get(GoveeLanBindingConstants.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15"); } } diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java new file mode 100644 index 0000000000000..58afaf7a34ed0 --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java @@ -0,0 +1,52 @@ +/** + * 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.goveelan.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.goveelan.internal.model.Color; +import org.openhab.binding.goveelan.internal.model.ColorData; +import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; +import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; +import org.openhab.binding.goveelan.internal.model.ValueData; + +import com.google.gson.Gson; + +/** + * @author Stefan Höhn - Initial contribution + */ +@NonNullByDefault +public class GoveeLanSerializeTest { + + 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}}}"; + + @Test + public void testSerializeMessage() { + GenericGoveeMessage lightOff = new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueData(0))); + assertEquals(lightOffJsonString, GSON.toJson(lightOff)); + GenericGoveeMessage lightOn = new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueData(100))); + assertEquals(lightOnJsonString, GSON.toJson(lightOn)); + GenericGoveeMessage lightColor = new GenericGoveeMessage( + new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 1, 2), 3))); + assertEquals(lightColorJsonString, GSON.toJson(lightColor)); + GenericGoveeMessage lightBrightness = new GenericGoveeMessage( + new GenericGoveeMsg("brightness", new ValueData(99))); + assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness)); + } +} From c78a406d9eea339ebf8d2ebc3c68b5d72d1aef0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 7 Oct 2023 15:20:39 +0200 Subject: [PATCH 07/29] small config fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../org/openhab/binding/goveelan/internal/GoveeLanHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index 15462c5a84a89..ca72dbe48b363 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -231,7 +231,7 @@ public void initialize() { goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); final String ipAddress = goveeLanConfiguration.hostname; - if (ipAddress.isEmpty()) { + if (!ipAddress.isEmpty()) { THING_HANDLERS.put(ipAddress, this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, From 115d33b6884248e02a588874fedde96b45928488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 08:17:29 +0200 Subject: [PATCH 08/29] power brignnhtness channel, last gson messages implemented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../model/EmptyValueQueryStatusData.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java new file mode 100644 index 0000000000000..62857b7f7382e --- /dev/null +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.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.goveelan.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 ValueData(int value) implements GenericGoveeData { +} From 9443370638f4677c9d3d05e074b2a24438f5c738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 09:02:55 +0200 Subject: [PATCH 09/29] add missing changed files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../org.openhab.binding.goveelan/README.md | 5 +- .../internal/GoveeLanConfiguration.java | 2 +- .../goveelan/internal/GoveeLanHandler.java | 55 ++++++------------- .../model/EmptyValueQueryStatusData.java | 8 +-- .../main/resources/OH-INF/config/config.xml | 2 +- .../resources/OH-INF/thing/thing-types.xml | 3 +- .../internal/GoveeLanSerializeTest.java | 5 ++ 7 files changed, 32 insertions(+), 48 deletions(-) diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md index d65992031a1e2..c709107f994bc 100644 --- a/bundles/org.openhab.binding.goveelan/README.md +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -114,10 +114,11 @@ arp -a | grep "MAC_ADDRESS" | Channel | Type | Read/Write | Description | |---------------------|-------------------|------------|---------------------| -| switch | Switch | RW | Power On / OFF | +| brightness | Percentage | RW | | +| | Switch | RW | Power On / OFF | | color | Color HSB Type | RW | | | colorTemperatureAbs | Color Temperature | RW | in 2000-9000 Kelvin | -| brightness | Percentage | RW | | + Note: you may have to add "%.0f K" as the state description when creating a colorTemperatureAbs item. diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java index ba7bed250a546..b2c20b2e65357 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java @@ -23,5 +23,5 @@ public class GoveeLanConfiguration { public String hostname = ""; - public int refreshInterval = 2; // in seconds + public int refreshInterval = 5; // in seconds } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index ca72dbe48b363..4a23ce4ec98c8 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -15,7 +15,6 @@ import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.BRIGHTNESS; import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR; import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR_TEMPERATURE_ABS; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.SWITCH; import java.io.IOException; import java.net.DatagramPacket; @@ -32,6 +31,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.goveelan.internal.model.Color; import org.openhab.binding.goveelan.internal.model.ColorData; +import org.openhab.binding.goveelan.internal.model.EmptyValueQueryStatusData; import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; import org.openhab.binding.goveelan.internal.model.StatusMessage; @@ -86,34 +86,6 @@ public class GoveeLanHandler extends BaseThingHandler { */ private static final Gson GSON = new Gson(); - private static final String LIGHT_OFF = GSON - .toJson(new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueData(0)))); - - // turning on via cmd-turn and value = 1 doesn't work, so let's use the brightness command - private static final String LIGHT_ON = GSON - .toJson(new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueData(100)))); - - private static final String LIGHT_BRIGHTNESS = """ - { - "msg":{ - "cmd":"brightness", - "data":{ - "value": %d - } - } - } - """; - - private static final String QUERY_STATUS = """ - { - "msg":{ - "cmd":"devStatus", - "data":{ - } - } - } - """; - // Holds a list of all thing handlers to send them thing updates via the receiver-Thread private static final Map THING_HANDLERS = new HashMap<>(); @@ -298,11 +270,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { triggerDeviceStatusRefresh(); } else { switch (channelUID.getId()) { - case SWITCH: - if (command instanceof OnOffType) { - send(command.equals(OnOffType.ON) ? LIGHT_ON : LIGHT_OFF); - } - break; case COLOR: if (command instanceof HSBType hsbCommand) { int[] rgb = ColorUtil.hsbToRgb(hsbCommand); @@ -320,7 +287,19 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case BRIGHTNESS: if (command instanceof PercentType percent) { - send(String.format(GoveeLanHandler.LIGHT_BRIGHTNESS, percent.intValue())); + GenericGoveeMessage lightBrightness = new GenericGoveeMessage( + new GenericGoveeMsg("brightness", new ValueData(percent.intValue()))); + send(GSON.toJson(lightBrightness)); + } else if (command instanceof OnOffType) { + if (command.equals(OnOffType.ON)) { + GenericGoveeMessage lightOn = new GenericGoveeMessage( + new GenericGoveeMsg("brightness", new ValueData(100))); + send(GSON.toJson(lightOn)); + } else { + GenericGoveeMessage lightOff = new GenericGoveeMessage( + new GenericGoveeMsg("turn", new ValueData(0))); + send(GSON.toJson(lightOff)); + } } break; } @@ -351,7 +330,9 @@ private void triggerDeviceStatusRefresh() throws IOException { LOGGER.debug("trigger Refresh Status of device {}", thing.getLabel()); try { - send(QUERY_STATUS); + GenericGoveeMessage lightQuery = new GenericGoveeMessage( + new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); + send(GSON.toJson(lightQuery)); } finally { refreshRunning = false; } @@ -380,9 +361,9 @@ public void updateDeviceState(@Nullable StatusMessage message) { Color lastColor = message.msg().data().color(); int lastColorTemperature = message.msg().data().colorTemInKelvin(); - updateState(SWITCH, OnOffType.from(lastOnOff == 1)); updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); updateState(BRIGHTNESS, new PercentType(lastBrightness)); + updateState(BRIGHTNESS, OnOffType.from(lastOnOff == 1)); } } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java index 62857b7f7382e..c41bc9c14a5ad 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java @@ -16,13 +16,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Simple Govee Value Data - * typically used for On / Off - * - * @param value + * Empty Govee Value Data + * Used to query device data * * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public record ValueData(int value) implements GenericGoveeData { +public record EmptyValueQueryStatusData() implements GenericGoveeData { } diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml index 8b66a07f8dfaa..f08e600667d06 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -25,7 +25,7 @@ The amount of time that passes until the device is refreshed (in seconds) - 2 + 5 diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml index 24b5a300b1128..982795adf0d39 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,10 +9,9 @@ Govee light controllable via LAN API - + - diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java index 58afaf7a34ed0..09c60d81928d7 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.openhab.binding.goveelan.internal.model.Color; import org.openhab.binding.goveelan.internal.model.ColorData; +import org.openhab.binding.goveelan.internal.model.EmptyValueQueryStatusData; import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; import org.openhab.binding.goveelan.internal.model.ValueData; @@ -35,6 +36,7 @@ public class GoveeLanSerializeTest { 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 static final String lightQueryJsonString = "{\"msg\":{\"cmd\":\"devStatus\",\"data\":{}}}"; @Test public void testSerializeMessage() { @@ -48,5 +50,8 @@ public void testSerializeMessage() { GenericGoveeMessage lightBrightness = new GenericGoveeMessage( new GenericGoveeMsg("brightness", new ValueData(99))); assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness)); + GenericGoveeMessage lightQuery = new GenericGoveeMessage( + new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); + assertEquals(lightQueryJsonString, GSON.toJson(lightQuery)); } } From ee7d3a86f1663e700d2c518f0eb904c27f1b39f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 10:52:15 +0200 Subject: [PATCH 10/29] change thing to thing-type:goveelan:govee-light MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../src/main/resources/OH-INF/config/config.xml | 2 +- .../src/main/resources/OH-INF/thing/thing-types.xml | 2 +- .../binding/goveelan/internal/GoveeLanSerializeTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml index f08e600667d06..12b98fab5c8f4 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -4,7 +4,7 @@ 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"> - + network-address diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml index 982795adf0d39..c581557cc4ffa 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -13,7 +13,7 @@ - + diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java index 09c60d81928d7..63006ab104281 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java @@ -36,7 +36,7 @@ public class GoveeLanSerializeTest { 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 static final String lightQueryJsonString = "{\"msg\":{\"cmd\":\"devStatus\",\"data\":{}}}"; + private final String lightQueryJsonString = "{\"msg\":{\"cmd\":\"devStatus\",\"data\":{}}}"; @Test public void testSerializeMessage() { From 5353e77c8554f889c2f3ab131f1a71e28245ac38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 12:02:59 +0200 Subject: [PATCH 11/29] further review changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.goveelan/README.md | 11 ++++++----- .../internal/GoveeLanBindingConstants.java | 2 +- .../goveelan/internal/GoveeLanHandler.java | 11 +++++++---- .../src/main/resources/OH-INF/config/config.xml | 6 +++--- .../resources/OH-INF/i18n/goveelan.properties | 4 ++-- .../main/resources/OH-INF/thing/thing-types.xml | 2 +- .../internal/GoveeLanDiscoveryTest.java | 17 ++++++++++++++++- 7 files changed, 36 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md index c709107f994bc..0446f15f8a9b7 100644 --- a/bundles/org.openhab.binding.goveelan/README.md +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -89,25 +89,26 @@ To be able to use the device with the LAN API, the following needs to be done (a 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. ++ 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 +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. | 3 | no | yes | +| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes | ## Channels @@ -120,7 +121,7 @@ arp -a | grep "MAC_ADDRESS" | colorTemperatureAbs | Color Temperature | RW | in 2000-9000 Kelvin | -Note: you may have to add "%.0f K" as the state description when creating a colorTemperatureAbs item. +Note: you may have to add "%.0f K" as the state description when creating a color-temperature-abs item. ## Additional Information diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java index 7ed4450c74cf7..18d06129cd653 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java @@ -25,8 +25,8 @@ public class GoveeLanBindingConstants { // Thing properties - public static final String IP_ADDRESS = "hostname"; 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"; private static final String BINDING_ID = "goveelan"; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index 4a23ce4ec98c8..769aaa57fd5a4 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -102,6 +102,7 @@ public class GoveeLanHandler extends BaseThingHandler { @Nullable private ScheduledFuture triggerStatusJob; // send device status update job private GoveeLanConfiguration goveeLanConfiguration = new GoveeLanConfiguration(); + private int lastBrightness; /* * Common Receiver job for the status answers of the devices @@ -155,7 +156,7 @@ public static boolean isRefreshJobRunning() { StatusMessage statusMessage = GSON.fromJson(response, StatusMessage.class); thingHandler.updateDeviceState(statusMessage); } catch (JsonSyntaxException jse) { - thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, jse.getMessage()); } } else { @@ -293,7 +294,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { GenericGoveeMessage lightOn = new GenericGoveeMessage( - new GenericGoveeMsg("brightness", new ValueData(100))); + new GenericGoveeMsg("brightness", new ValueData(lastBrightness))); send(GSON.toJson(lightOn)); } else { GenericGoveeMessage lightOff = new GenericGoveeMessage( @@ -357,13 +358,15 @@ public void updateDeviceState(@Nullable StatusMessage message) { } int lastOnOff = message.msg().data().onOff(); - int lastBrightness = message.msg().data().brightness(); + lastBrightness = message.msg().data().brightness(); Color lastColor = message.msg().data().color(); int lastColorTemperature = message.msg().data().colorTemInKelvin(); updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); - updateState(BRIGHTNESS, new PercentType(lastBrightness)); + LOGGER.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); updateState(BRIGHTNESS, OnOffType.from(lastOnOff == 1)); + LOGGER.debug("setting BRIGHTNESS to PercentType", new PercentType(lastBrightness)); + updateState(BRIGHTNESS, new PercentType(lastBrightness)); } } diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml index 12b98fab5c8f4..3a90d2abb120c 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml @@ -7,8 +7,8 @@ network-address - - Hostname or IP Address of the device + + Hostname or IP address of the device @@ -23,7 +23,7 @@ Description of the device - + The amount of time that passes until the device is refreshed (in seconds) 5 diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties index 808492c09b826..c2533c0bb3805 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties @@ -15,8 +15,8 @@ thing-type.config.goveelan.govee-light.refreshInterval.description = The amount # channel types -channel-type.goveelan.colortemperatureabs.label = Color Temperature (Absolute) -channel-type.goveelan.colortemperatureabs.description = Controls the color temperature of the light in Kelvin +channel-type.goveelan.color-temperature-abs.label = Color Temperature (Absolute) +channel-type.goveelan.color-temperature-abs.description = Controls the color temperature of the light in Kelvin # product names diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml index c581557cc4ffa..80e7ef863ec9f 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + Govee light controllable via LAN API diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java index 77799596fd67a..c713105995fd0 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java +++ b/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java @@ -26,7 +26,22 @@ @NonNullByDefault public class GoveeLanDiscoveryTest { - 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\"}}}"; + 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() { From 354f55bfda68303c55dfca6c698dd10f27ee53e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 12:05:49 +0200 Subject: [PATCH 12/29] spotless issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.goveelan/README.md | 1 + .../org/openhab/binding/goveelan/internal/GoveeLanHandler.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.goveelan/README.md index 0446f15f8a9b7..435b3e9344127 100644 --- a/bundles/org.openhab.binding.goveelan/README.md +++ b/bundles/org.openhab.binding.goveelan/README.md @@ -101,6 +101,7 @@ One possibility is to look for the MAC address in the Govee app and then looking ```shell arp -a | grep "MAC_ADDRESS" ``` + ### `govee-light` Thing Configuration | Name | Type | Description | Default | Required | Advanced | diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java index 769aaa57fd5a4..b9daf1601768c 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java @@ -366,7 +366,7 @@ public void updateDeviceState(@Nullable StatusMessage message) { updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); LOGGER.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); updateState(BRIGHTNESS, OnOffType.from(lastOnOff == 1)); - LOGGER.debug("setting BRIGHTNESS to PercentType", new PercentType(lastBrightness)); + LOGGER.debug("setting BRIGHTNESS to PercentType {}", new PercentType(lastBrightness)); updateState(BRIGHTNESS, new PercentType(lastBrightness)); } } From 00740a6a9b5bfe9f6ef7ba292aa9b5e60fca43f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 8 Oct 2023 22:15:02 +0200 Subject: [PATCH 13/29] rename from goveelan to govee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- CODEOWNERS | 2 +- bom/openhab-addons/pom.xml | 2 +- .../NOTICE | 0 .../README.md | 0 .../doc/device-settings.png | Bin .../doc/govee-lights.png | Bin .../pom.xml | 4 +- .../src/main/feature/feature.xml | 6 +- .../internal/GoveeBindingConstants.java} | 8 +- .../govee/internal/GoveeConfiguration.java} | 6 +- .../internal/GoveeDiscoveryService.java} | 64 +++++++------ .../binding/govee/internal/GoveeHandler.java} | 74 +++++++-------- .../govee/internal/GoveeHandlerFactory.java} | 12 +-- .../binding/govee}/internal/model/Color.java | 2 +- .../govee}/internal/model/ColorData.java | 2 +- .../govee}/internal/model/DiscoveryData.java | 2 +- .../internal/model/DiscoveryMessage.java | 2 +- .../govee}/internal/model/DiscoveryMsg.java | 2 +- .../model/EmptyValueQueryStatusData.java | 2 +- .../internal/model/GenericGoveeData.java | 2 +- .../internal/model/GenericGoveeMessage.java | 2 +- .../internal/model/GenericGoveeMsg.java | 2 +- .../govee}/internal/model/StatusData.java | 2 +- .../govee}/internal/model/StatusMessage.java | 2 +- .../govee}/internal/model/StatusMsg.java | 2 +- .../govee/internal/model/ValueIntData.java} | 4 +- .../govee/internal/model/ValueStringData.java | 28 ++++++ .../src/main/resources/OH-INF/addon/addon.xml | 2 +- .../main/resources/OH-INF/config/config.xml | 2 +- .../resources/OH-INF/i18n/govee.properties | 85 ++++++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 4 +- .../govee/internal/GoveeDiscoveryTest.java} | 12 +-- .../govee/internal/GoveeSerializeTest.java} | 22 ++--- .../resources/OH-INF/i18n/goveelan.properties | 85 ------------------ bundles/pom.xml | 2 +- 35 files changed, 243 insertions(+), 205 deletions(-) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/NOTICE (100%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/README.md (100%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/doc/device-settings.png (100%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/doc/govee-lights.png (100%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/pom.xml (80%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/src/main/feature/feature.xml (52%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java} (84%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java} (77%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java} (79%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java} (83%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java} (77%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/Color.java (91%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/ColorData.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/DiscoveryData.java (95%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/DiscoveryMessage.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/DiscoveryMsg.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/EmptyValueQueryStatusData.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/GenericGoveeData.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/GenericGoveeMessage.java (91%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/GenericGoveeMsg.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/StatusData.java (92%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/StatusMessage.java (90%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee}/internal/model/StatusMsg.java (91%) rename bundles/{org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java => org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java} (83%) create mode 100644 bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/src/main/resources/OH-INF/addon/addon.xml (83%) rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/src/main/resources/OH-INF/config/config.xml (95%) create mode 100644 bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties rename bundles/{org.openhab.binding.goveelan => org.openhab.binding.govee}/src/main/resources/OH-INF/thing/thing-types.xml (90%) rename bundles/{org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java => org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java} (75%) rename bundles/{org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java => org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java} (76%) delete mode 100644 bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties diff --git a/CODEOWNERS b/CODEOWNERS index 3e01c978bd055..b3c666c0941c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -122,7 +122,7 @@ /bundles/org.openhab.binding.generacmobilelink/ @digitaldan /bundles/org.openhab.binding.globalcache/ @mhilbush /bundles/org.openhab.binding.goecharger/ @SamuelBrucksch -/bundles/org.openhab.binding.goveelan/ @stefan-hoehn +/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 dfadb2d3f0e34..402dbf1be82c5 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -603,7 +603,7 @@ org.openhab.addons.bundles - org.openhab.binding.goveelan + org.openhab.binding.govee ${project.version} diff --git a/bundles/org.openhab.binding.goveelan/NOTICE b/bundles/org.openhab.binding.govee/NOTICE similarity index 100% rename from bundles/org.openhab.binding.goveelan/NOTICE rename to bundles/org.openhab.binding.govee/NOTICE diff --git a/bundles/org.openhab.binding.goveelan/README.md b/bundles/org.openhab.binding.govee/README.md similarity index 100% rename from bundles/org.openhab.binding.goveelan/README.md rename to bundles/org.openhab.binding.govee/README.md diff --git a/bundles/org.openhab.binding.goveelan/doc/device-settings.png b/bundles/org.openhab.binding.govee/doc/device-settings.png similarity index 100% rename from bundles/org.openhab.binding.goveelan/doc/device-settings.png rename to bundles/org.openhab.binding.govee/doc/device-settings.png diff --git a/bundles/org.openhab.binding.goveelan/doc/govee-lights.png b/bundles/org.openhab.binding.govee/doc/govee-lights.png similarity index 100% rename from bundles/org.openhab.binding.goveelan/doc/govee-lights.png rename to bundles/org.openhab.binding.govee/doc/govee-lights.png diff --git a/bundles/org.openhab.binding.goveelan/pom.xml b/bundles/org.openhab.binding.govee/pom.xml similarity index 80% rename from bundles/org.openhab.binding.goveelan/pom.xml rename to bundles/org.openhab.binding.govee/pom.xml index b5a57134cce44..6a7245634acb3 100644 --- a/bundles/org.openhab.binding.goveelan/pom.xml +++ b/bundles/org.openhab.binding.govee/pom.xml @@ -10,8 +10,8 @@ 4.1.0-SNAPSHOT - org.openhab.binding.goveelan + org.openhab.binding.govee - openHAB Add-ons :: Bundles :: GoveeLan Binding + openHAB Add-ons :: Bundles :: Govee Binding diff --git a/bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml similarity index 52% rename from bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml rename to bundles/org.openhab.binding.govee/src/main/feature/feature.xml index 094ece4302d1a..8d0ae40c5d9fc 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml @@ -1,9 +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.goveelan/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.govee/${project.version} diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java similarity index 84% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java index 18d06129cd653..72cfcbe174f10 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanBindingConstants.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java @@ -10,26 +10,26 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; /** - * The {@link GoveeLanBindingConstants} class defines common constants, which are + * The {@link GoveeBindingConstants} class defines common constants, which are * used across the whole binding. * * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public class GoveeLanBindingConstants { +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"; - private static final String BINDING_ID = "goveelan"; + 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"); diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java similarity index 77% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java index b2c20b2e65357..319cb573e2539 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanConfiguration.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java @@ -10,17 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link GoveeLanConfiguration} contains thing values that are used by the Thing Handler + * The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler * * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public class GoveeLanConfiguration { +public class GoveeConfiguration { public String hostname = ""; public int refreshInterval = 5; // in seconds diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java similarity index 79% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java index f104eb3ddc5c9..6afe221a372c3 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryService.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; import java.io.IOException; import java.net.DatagramPacket; @@ -21,6 +21,7 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -30,7 +31,7 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.goveelan.internal.model.DiscoveryMessage; +import org.openhab.binding.govee.internal.model.DiscoveryMessage; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; @@ -75,18 +76,18 @@ * } * * Note that it uses the same port for receiving data like when receiving devices status updates. - * - * @see GoveeLanHandler + * + * @see GoveeHandler * * @author Stefan Höhn - Initial Contribution */ @NonNullByDefault -@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.goveelan") -public class GoveeLanDiscoveryService extends AbstractDiscoveryService { +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee") +public class GoveeDiscoveryService extends AbstractDiscoveryService { public static boolean discoveryActive = false; - private final Logger logger = LoggerFactory.getLogger(GoveeLanDiscoveryService.class); + private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class); private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250"; private static final int DISCOVERY_PORT = 4001; @@ -96,17 +97,16 @@ public class GoveeLanDiscoveryService extends AbstractDiscoveryService { private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}"; - private static final Set SUPPORTED_THING_TYPES_UIDS = Set - .of(GoveeLanBindingConstants.THING_TYPE_LIGHT); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT); @Activate - public GoveeLanDiscoveryService(@Reference TranslationProvider i18nProvider) throws IllegalArgumentException { + public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider) throws IllegalArgumentException { super(SUPPORTED_THING_TYPES_UIDS, 0, false); this.i18nProvider = i18nProvider; } // for test purposes only - public GoveeLanDiscoveryService() { + public GoveeDiscoveryService() { super(SUPPORTED_THING_TYPES_UIDS, 0, false); } @@ -119,30 +119,36 @@ protected void startScan() { // check if the status receiver is currently running, stop it and wait for that. // note that it restarts itself as soon as we are done. - while (GoveeLanHandler.isRefreshJobRunning()) { - GoveeLanHandler.stopRefreshStatusJob(); + while (GoveeHandler.isRefreshJobRunning()) { + GoveeHandler.stopRefreshStatusJob(); Thread.sleep(1000); logger.debug("Waiting for device status request finish its task to be able to start discovery"); } + InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); + final InetSocketAddress socketAddress = new InetSocketAddress(broadcastAddress, DISCOVERY_RESPONSE_PORT); + getLocalNetworkInterfaces().forEach(localNetworkInterface -> { logger.debug("Discovering Govee Devices on {} ...", localNetworkInterface); - try (MulticastSocket sendSocket = new MulticastSocket(); + + try (MulticastSocket sendSocket = new MulticastSocket(socketAddress); MulticastSocket receiveSocket = new MulticastSocket(DISCOVERY_RESPONSE_PORT)) { sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); + sendSocket.setReuseAddress(true); sendSocket.setBroadcast(true); sendSocket.setTimeToLive(2); - InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, DISCOVERY_RESPONSE_PORT), localNetworkInterface); - + receiveSocket.setReuseAddress(true); sendBroadcastToDiscoverThing(sendSocket, receiveSocket, broadcastAddress); } catch (IOException e) { logger.warn("Discovery with IO exception: {}", e.getMessage()); } }); - } catch (InterruptedException e) { + } catch (InterruptedException ie) { // don't care + } catch (UnknownHostException e) { + logger.warn("Discovery with UnknownHostException exception: {}", e.getMessage()); } finally { discoveryActive = false; } @@ -171,24 +177,24 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS logger.trace("Govee Device Response: {}", response); final Map properties = getDeviceProperties(response); - final Object product = properties.get(GoveeLanBindingConstants.PRODUCT_NAME); + final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); final String productName = (product != null) ? product.toString() : "unknown"; - final Object mac = properties.get(GoveeLanBindingConstants.MAC_ADDRESS); + final Object mac = properties.get(GoveeBindingConstants.MAC_ADDRESS); final String macAddress = (mac != null) ? mac.toString() : "unknown"; - ThingUID thingUid = new ThingUID(GoveeLanBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_")); + ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_")); DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid).withProperties(properties) - .withRepresentationProperty(GoveeLanBindingConstants.MAC_ADDRESS) - .withLabel("Govee " + productName + " " + properties.get(GoveeLanBindingConstants.DEVICE_TYPE) - + " (" + properties.get(GoveeLanBindingConstants.IP_ADDRESS) + ")"); + .withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) + .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) + " (" + + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); thingDiscovered(discoveryResult.build()); } while (true); // left by SocketTimeoutException } public Map getDeviceProperties(String response) { - Bundle bundle = FrameworkUtil.getBundle(GoveeLanDiscoveryService.class); + Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class); Gson gson = new Gson(); DiscoveryMessage message = gson.fromJson(response, DiscoveryMessage.class); @@ -209,7 +215,7 @@ public Map getDeviceProperties(String response) { productName = "unknown"; if (!sku.isEmpty()) { - final String skuLabel = "discovery.goveelan.govee-light." + sku; + final String skuLabel = "discovery.govee-light." + sku; if (bundle != null) { productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); } @@ -226,10 +232,10 @@ public Map getDeviceProperties(String response) { } Map properties = new HashMap<>(3); - properties.put(GoveeLanBindingConstants.IP_ADDRESS, ipAddress); - properties.put(GoveeLanBindingConstants.DEVICE_TYPE, sku); - properties.put(GoveeLanBindingConstants.MAC_ADDRESS, macAddress); - properties.put(GoveeLanBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); + properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); + properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); + properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); + properties.put(GoveeBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); return properties; } diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java similarity index 83% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java index b9daf1601768c..b1400515f0ea8 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandler.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.BRIGHTNESS; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.COLOR_TEMPERATURE_ABS; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.BRIGHTNESS; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_ABS; import java.io.IOException; import java.net.DatagramPacket; @@ -29,13 +29,14 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.goveelan.internal.model.Color; -import org.openhab.binding.goveelan.internal.model.ColorData; -import org.openhab.binding.goveelan.internal.model.EmptyValueQueryStatusData; -import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; -import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; -import org.openhab.binding.goveelan.internal.model.StatusMessage; -import org.openhab.binding.goveelan.internal.model.ValueData; +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.GenericGoveeMessage; +import org.openhab.binding.govee.internal.model.GenericGoveeMsg; +import org.openhab.binding.govee.internal.model.StatusMessage; +import org.openhab.binding.govee.internal.model.ValueIntData; +import org.openhab.binding.govee.internal.model.ValueStringData; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; @@ -57,7 +58,7 @@ import com.google.gson.JsonSyntaxException; /** - * The {@link GoveeLanHandler} is responsible for handling commands, which are + * 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. @@ -79,7 +80,7 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public class GoveeLanHandler extends BaseThingHandler { +public class GoveeHandler extends BaseThingHandler { /* * Messages to be sent to the Govee Devices @@ -87,9 +88,9 @@ public class GoveeLanHandler extends BaseThingHandler { private static final Gson GSON = new Gson(); // Holds a list of all thing handlers to send them thing updates via the receiver-Thread - private static final Map THING_HANDLERS = new HashMap<>(); + private static final Map THING_HANDLERS = new HashMap<>(); - private final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); + private final Logger LOGGER = LoggerFactory.getLogger(GoveeHandler.class); private static final int SENDTODEVICE_PORT = 4003; private static final int RECEIVEFROMDEVICE_PORT = 4002; @@ -101,7 +102,7 @@ public class GoveeLanHandler extends BaseThingHandler { private static ScheduledFuture refreshStatusJob; // device response receiver job @Nullable private ScheduledFuture triggerStatusJob; // send device status update job - private GoveeLanConfiguration goveeLanConfiguration = new GoveeLanConfiguration(); + private GoveeConfiguration goveeConfiguration = new GoveeConfiguration(); private int lastBrightness; /* @@ -112,14 +113,14 @@ public static boolean isRefreshJobRunning() { } private static final Runnable REFRESH_STATUS_RECEIVER = () -> { - final Logger LOGGER = LoggerFactory.getLogger(GoveeLanHandler.class); + final Logger LOGGER = LoggerFactory.getLogger(GoveeHandler.class); /* * This thread receives an answer from any device. * Therefore it needs to apply it to the right thing * * Discovery uses the same response code, so we must not refresh the status during discovery */ - if (GoveeLanDiscoveryService.isDiscoveryActive()) { + if (GoveeDiscoveryService.isDiscoveryActive()) { LOGGER.debug("Not running refresh as Scan is currently active"); } @@ -130,7 +131,8 @@ public static boolean isRefreshJobRunning() { return; } - GoveeLanHandler thingHandler; + GoveeHandler thingHandler; + try (MulticastSocket socket = new MulticastSocket(RECEIVEFROMDEVICE_PORT)) { byte[] buffer = new byte[10240]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); @@ -161,7 +163,7 @@ public static boolean isRefreshJobRunning() { } } else { thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.offline.communication-error.empty-response"); + "@text/offline.communication-error.empty-response"); } if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { thingHandler.updateStatus(ThingStatus.ONLINE); @@ -191,24 +193,25 @@ public static boolean isRefreshJobRunning() { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.could-not-query-device [\"" + goveeLanConfiguration.hostname + "\"]"); + "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname + + "\"]"); } }; - public GoveeLanHandler(Thing thing) { + public GoveeHandler(Thing thing) { super(thing); } @Override public void initialize() { - goveeLanConfiguration = getConfigAs(GoveeLanConfiguration.class); + goveeConfiguration = getConfigAs(GoveeConfiguration.class); - final String ipAddress = goveeLanConfiguration.hostname; + final String ipAddress = goveeConfiguration.hostname; if (!ipAddress.isEmpty()) { THING_HANDLERS.put(ipAddress, this); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/error.offline.ip-address.missing"); + "@text/offline.configuration-error.ip-address.missing"); return; } if (!THING_HANDLERS.isEmpty()) { @@ -219,7 +222,7 @@ public void initialize() { LOGGER.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100, - goveeLanConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS); + goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS); } updateStatus(ThingStatus.UNKNOWN); @@ -254,8 +257,8 @@ public void dispose() { triggerStatusJob.cancel(true); triggerStatusJob = null; } - if (!goveeLanConfiguration.hostname.isEmpty()) { - THING_HANDLERS.remove(goveeLanConfiguration.hostname); + if (!goveeConfiguration.hostname.isEmpty()) { + THING_HANDLERS.remove(goveeConfiguration.hostname); } if (THING_HANDLERS.isEmpty()) { @@ -289,16 +292,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { case BRIGHTNESS: if (command instanceof PercentType percent) { GenericGoveeMessage lightBrightness = new GenericGoveeMessage( - new GenericGoveeMsg("brightness", new ValueData(percent.intValue()))); + new GenericGoveeMsg("brightness", new ValueIntData(percent.intValue()))); send(GSON.toJson(lightBrightness)); } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { GenericGoveeMessage lightOn = new GenericGoveeMessage( - new GenericGoveeMsg("brightness", new ValueData(lastBrightness))); + new GenericGoveeMsg("turn", new ValueStringData("on"))); send(GSON.toJson(lightOn)); } else { GenericGoveeMessage lightOff = new GenericGoveeMessage( - new GenericGoveeMsg("turn", new ValueData(0))); + new GenericGoveeMsg("turn", new ValueStringData("off"))); send(GSON.toJson(lightOff)); } } @@ -310,7 +313,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.could-not-query-device [\"" + goveeLanConfiguration.hostname + "\"]"); + "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname + + "\"]"); } } @@ -322,7 +326,7 @@ private void triggerDeviceStatusRefresh() throws IOException { if (refreshRunning) { return; } - if (GoveeLanDiscoveryService.isDiscoveryActive()) { + if (GoveeDiscoveryService.isDiscoveryActive()) { LOGGER.debug("Not triggering refresh as Scan is currently active"); return; } @@ -345,8 +349,8 @@ public void send(String message) throws IOException { socket.setReuseAddress(true); byte[] data = message.getBytes(); - InetAddress address = InetAddress.getByName(goveeLanConfiguration.hostname); - LOGGER.trace("Sending {} to {}", message, goveeLanConfiguration.hostname); + InetAddress address = InetAddress.getByName(goveeConfiguration.hostname); + LOGGER.trace("Sending {} to {}", message, goveeConfiguration.hostname); DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); socket.send(packet); socket.close(); diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java similarity index 77% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java index fec316705c59a..e8ad92330b3aa 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/GoveeLanHandlerFactory.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java @@ -10,9 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; -import static org.openhab.binding.goveelan.internal.GoveeLanBindingConstants.*; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.*; import java.util.Set; @@ -26,14 +26,14 @@ import org.osgi.service.component.annotations.Component; /** - * The {@link GoveeLanHandlerFactory} is responsible for creating things and thing + * The {@link GoveeHandlerFactory} is responsible for creating things and thing * handlers. * * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -@Component(configurationPid = "binding.goveelan", service = ThingHandlerFactory.class) -public class GoveeLanHandlerFactory extends BaseThingHandlerFactory { +@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); @Override @@ -46,7 +46,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_LIGHT.equals(thingTypeUID)) { - return new GoveeLanHandler(thing); + return new GoveeHandler(thing); } return null; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java similarity index 91% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java index a93ddb3ccd106..88c8dd4bbe659 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/Color.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; /** * diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java index 05fd2ac6d6609..c46fded8ab77a 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ColorData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java similarity index 95% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java index 92e3057ae5c02..04b7b90d643dd 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java index f757894f5ecda..1a40cb458b680 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java index 878cfadebdafb..f5bf218a4934f 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/DiscoveryMsg.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java index c41bc9c14a5ad..e04b4a0c174db 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/EmptyValueQueryStatusData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java index bd7a6fd433d57..bd8f6af1658cd 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMessage.java similarity index 91% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMessage.java index a1c587628dc6d..7d04b1b15b892 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMessage.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java index eb9f0ada7fcb1..89a5b31d5837d 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/GenericGoveeMsg.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java similarity index 92% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java index 72909f62e72c4..5d353739a8888 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; /** * diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java similarity index 90% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java index 1623a00bdebb6..94a577e5c7310 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; /** * Govee Message diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java similarity index 91% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java index 8e5c21fbecaca..8beb13388ee13 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/StatusMsg.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; /** * Govee Message - Cmd diff --git a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java similarity index 83% rename from bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java index 62857b7f7382e..81dcf25ef91ff 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/java/org/openhab/binding/goveelan/internal/model/ValueData.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 * */ -package org.openhab.binding.goveelan.internal.model; +package org.openhab.binding.govee.internal.model; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -24,5 +24,5 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public record ValueData(int value) implements GenericGoveeData { +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.goveelan/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml similarity index 83% rename from bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml rename to bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml index 4d7d1cb3ec097..111e51af14da0 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml @@ -1,5 +1,5 @@ - diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml similarity index 95% rename from bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml rename to bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml index 3a90d2abb120c..7f339eb8d5fe9 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml @@ -4,7 +4,7 @@ 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"> - + network-address 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..8eee24ae708ba --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties @@ -0,0 +1,85 @@ +# 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 + +# channel types + +channel-type.color-temperature-abs.label = Color Temperature (Absolute) +channel-type.color-temperature-abs.description = Controls the color temperature of the light in Kelvin + +# 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 = Handler for thing could not be added to list because ipaddress == null +offline.communication-error.empty-response = Offline due to receiving an empty response diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml similarity index 90% rename from bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml rename to bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml index 80e7ef863ec9f..2e9b4a337b391 100644 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml @@ -1,5 +1,5 @@ - @@ -13,7 +13,7 @@ - + diff --git a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java similarity index 75% rename from bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java rename to bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java index c713105995fd0..2cee266ba10f2 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanDiscoveryTest.java +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +package org.openhab.binding.govee.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -24,7 +24,7 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public class GoveeLanDiscoveryTest { +public class GoveeDiscoveryTest { String response = """ { @@ -45,11 +45,11 @@ public class GoveeLanDiscoveryTest { @Test public void testProcessScanMessage() { - GoveeLanDiscoveryService service = new GoveeLanDiscoveryService(); + GoveeDiscoveryService service = new GoveeDiscoveryService(); Map deviceProperties = service.getDeviceProperties(response); assertNotNull(deviceProperties); - assertEquals(deviceProperties.get(GoveeLanBindingConstants.DEVICE_TYPE), "H6076"); - assertEquals(deviceProperties.get(GoveeLanBindingConstants.IP_ADDRESS), "192.168.178.171"); - assertEquals(deviceProperties.get(GoveeLanBindingConstants.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15"); + 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.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java similarity index 76% rename from bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java rename to bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java index 63006ab104281..1c8cee30ae97c 100644 --- a/bundles/org.openhab.binding.goveelan/src/test/java/org/openhab/binding/goveelan/internal/GoveeLanSerializeTest.java +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java @@ -10,18 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.goveelan.internal; +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.goveelan.internal.model.Color; -import org.openhab.binding.goveelan.internal.model.ColorData; -import org.openhab.binding.goveelan.internal.model.EmptyValueQueryStatusData; -import org.openhab.binding.goveelan.internal.model.GenericGoveeMessage; -import org.openhab.binding.goveelan.internal.model.GenericGoveeMsg; -import org.openhab.binding.goveelan.internal.model.ValueData; +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.GenericGoveeMessage; +import org.openhab.binding.govee.internal.model.GenericGoveeMsg; +import org.openhab.binding.govee.internal.model.ValueIntData; import com.google.gson.Gson; @@ -29,7 +29,7 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public class GoveeLanSerializeTest { +public class GoveeSerializeTest { private static final Gson GSON = new Gson(); private final String lightOffJsonString = "{\"msg\":{\"cmd\":\"turn\",\"data\":{\"value\":0}}}"; @@ -40,15 +40,15 @@ public class GoveeLanSerializeTest { @Test public void testSerializeMessage() { - GenericGoveeMessage lightOff = new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueData(0))); + GenericGoveeMessage lightOff = new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueIntData(0))); assertEquals(lightOffJsonString, GSON.toJson(lightOff)); - GenericGoveeMessage lightOn = new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueData(100))); + GenericGoveeMessage lightOn = new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueIntData(100))); assertEquals(lightOnJsonString, GSON.toJson(lightOn)); GenericGoveeMessage lightColor = new GenericGoveeMessage( new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 1, 2), 3))); assertEquals(lightColorJsonString, GSON.toJson(lightColor)); GenericGoveeMessage lightBrightness = new GenericGoveeMessage( - new GenericGoveeMsg("brightness", new ValueData(99))); + new GenericGoveeMsg("brightness", new ValueIntData(99))); assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness)); GenericGoveeMessage lightQuery = new GenericGoveeMessage( new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); diff --git a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties b/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties deleted file mode 100644 index c2533c0bb3805..0000000000000 --- a/bundles/org.openhab.binding.goveelan/src/main/resources/OH-INF/i18n/goveelan.properties +++ /dev/null @@ -1,85 +0,0 @@ -# add-on - -addon.goveelan.name = GoveeLan Binding -addon.goveelan.description = This is the binding for handling Govee Lights via the LAN-API interface. - -# thing types - -thing-type.goveelan.govee-light.label = Govee Light Thing -thing-type.goveelan.govee-light.description = Govee Light controllable via LAN API - -# thing types config - -thing-type.config.goveelan.govee-light.refreshInterval.label = Light refresh interval (sec) -thing-type.config.goveelan.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed - -# channel types - -channel-type.goveelan.color-temperature-abs.label = Color Temperature (Absolute) -channel-type.goveelan.color-temperature-abs.description = Controls the color temperature of the light in Kelvin - -# product names - -discovery.goveelan.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights -discovery.goveelan.govee-light.H6046 = H6046 RGBIC TV Light Bars -discovery.goveelan.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller -discovery.goveelan.govee-light.H6061 = H6061 Glide Hexa LED Panels -discovery.goveelan.govee-light.H6062 = H6062 Glide Wall Light -discovery.goveelan.govee-light.H6065 = H6065 Glide RGBIC Y Lights -discovery.goveelan.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel -discovery.goveelan.govee-light.H6067 = H6067 Glide Triangle Light Panels -discovery.goveelan.govee-light.H6072 = H6072 RGBICWW Corner Floor Lamp -discovery.goveelan.govee-light.H6076 = H6076 RGBICW Smart Corner Floor Lamp -discovery.goveelan.govee-light.H6073 = H6073 LED Floor Lamp -discovery.goveelan.govee-light.H6078 = H6078 Cylinder Floor Lamp -discovery.goveelan.govee-light.H6087 = H6087 RGBIC Smart Wall Sconces -discovery.goveelan.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights -discovery.goveelan.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M -discovery.goveelan.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating -discovery.goveelan.govee-light.H619C = H619C LED Strip Lights With Protective Coating -discovery.goveelan.govee-light.H619D = H619D RGBIC PRO LED Strip Lights -discovery.goveelan.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating -discovery.goveelan.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M -discovery.goveelan.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M -discovery.goveelan.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M -discovery.goveelan.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light -discovery.goveelan.govee-light.H61A5 = H61A5 Neon LED Strip Light 10 -discovery.goveelan.govee-light.H61A8 = H61A8Neon Neon Rope Light 10 -discovery.goveelan.govee-light.H618A = H618A RGBIC Basic LED Strip Lights 5M -discovery.goveelan.govee-light.H618C = H618C RGBIC Basic LED Strip Lights 5M -discovery.goveelan.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M -discovery.goveelan.govee-light.H6159 = H6159 RGB Light Strip -discovery.goveelan.govee-light.H615E = H615E LED Strip Lights 30M -discovery.goveelan.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M -discovery.goveelan.govee-light.H610A = H610A Glide Lively Wall Lights -discovery.goveelan.govee-light.H610B = H610B Music Wall Lights -discovery.goveelan.govee-light.H6172 = H6172 Outdoor LED Strip 10m -discovery.goveelan.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight -discovery.goveelan.govee-light.H61E1 = H61E1 LED Strip Light M1 -discovery.goveelan.govee-light.H7012 = H7012 Warm White Outdoor String Lights -discovery.goveelan.govee-light.H7013 = H7013 Warm White Outdoor String Lights -discovery.goveelan.govee-light.H7021 = H7021 RGBIC Warm White Smart Outdoor String -discovery.goveelan.govee-light.H7028 = H7028 Lynx Dream LED-Bulb String -discovery.goveelan.govee-light.H7041 = H7041 LED Outdoor Bulb String Lights -discovery.goveelan.govee-light.H7042 = H7042 LED Outdoor Bulb String Lights -discovery.goveelan.govee-light.H705A = H705A Permanent Outdoor Lights 30M -discovery.goveelan.govee-light.H705B = H705B Permanent Outdoor Lights 15M -discovery.goveelan.govee-light.H7050 = H7050 Outdoor Ground Lights 11M -discovery.goveelan.govee-light.H7051 = H7051 Outdoor Ground Lights 15M -discovery.goveelan.govee-light.H7055 = H7055 Pathway Light -discovery.goveelan.govee-light.H7060 = H7060 LED Flood Lights (2-Pack) -discovery.goveelan.govee-light.H7061 = H7061 LED Flood Lights (4-Pack) -discovery.goveelan.govee-light.H7062 = H7062 LED Flood Lights (6-Pack) -discovery.goveelan.govee-light.H7065 = H7065 Outdoor Spot Lights -discovery.goveelan.govee-light.H6051 = H6051 Aura - Smart Table Lamp -discovery.goveelan.govee-light.H6056 = H6056 H6056 Flow Plus -discovery.goveelan.govee-light.H6059 = H6059 RGBWW Night Light for Kids -discovery.goveelan.govee-light.H618F = H618F RGBIC LED Strip Lights -discovery.goveelan.govee-light.H618E = H618E LED Strip Lights 22m -discovery.goveelan.govee-light.H6168 = H6168 TV LED Backlight - -# thing status descriptions - -error.could-not-query-device = Could not control/query device at IP address {0} -error.offline.ip-address.missing = Handler for thing could not be added to list because ipaddress == null -error.offline.communication-error.empty-response = Offline due to receiving an empty response diff --git a/bundles/pom.xml b/bundles/pom.xml index fba1de4da41be..22f5c77c84628 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -153,7 +153,7 @@ org.openhab.binding.gce org.openhab.binding.generacmobilelink org.openhab.binding.goecharger - org.openhab.binding.goveelan + org.openhab.binding.govee org.openhab.binding.gpio org.openhab.binding.globalcache org.openhab.binding.gpstracker From 3e0ec5157926546f2a73599d84f337218b0ea4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Thu, 12 Oct 2023 08:54:51 +0200 Subject: [PATCH 14/29] add color-percentage, fix color msg, refactor model names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 13 +++---- bundles/org.openhab.binding.govee/pom.xml | 15 ++++++++ .../govee/internal/GoveeBindingConstants.java | 3 ++ .../govee/internal/GoveeDiscoveryService.java | 4 +-- .../binding/govee/internal/GoveeHandler.java | 36 ++++++++++++------- ...eryMessage.java => DiscoveryResponse.java} | 4 +-- ...eMessage.java => GenericGoveeRequest.java} | 2 +- ...StatusMessage.java => StatusResponse.java} | 2 +- .../resources/OH-INF/thing/thing-types.xml | 1 + .../govee/internal/GoveeSerializeTest.java | 12 +++---- 10 files changed, 62 insertions(+), 30 deletions(-) rename bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/{DiscoveryMessage.java => DiscoveryResponse.java} (88%) rename bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/{GenericGoveeMessage.java => GenericGoveeRequest.java} (91%) rename bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/{StatusMessage.java => StatusResponse.java} (91%) diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index 435b3e9344127..fe10eee21bc19 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -114,12 +114,13 @@ arp -a | grep "MAC_ADDRESS" ## Channels -| Channel | Type | Read/Write | Description | -|---------------------|-------------------|------------|---------------------| -| brightness | Percentage | RW | | -| | Switch | RW | Power On / OFF | -| color | Color HSB Type | RW | | -| colorTemperatureAbs | Color Temperature | RW | in 2000-9000 Kelvin | +| Channel | Type | Read/Write | Description | +|-----------------------|-------------------|------------|---------------------| +| brightness | Percentage | RW | | +| | Switch | RW | Power On / OFF | +| color | Color HSB Type | RW | | +| color-temperature | Percentage | RW | | +| color-temperature-abs | Color Temperature | RW | in 2000-9000 Kelvin | Note: you may have to add "%.0f K" as the state description when creating a color-temperature-abs item. diff --git a/bundles/org.openhab.binding.govee/pom.xml b/bundles/org.openhab.binding.govee/pom.xml index 6a7245634acb3..00bef3961d934 100644 --- a/bundles/org.openhab.binding.govee/pom.xml +++ b/bundles/org.openhab.binding.govee/pom.xml @@ -10,6 +10,21 @@ 4.1.0-SNAPSHOT + + + org.openhab.binding.govee openHAB Add-ons :: Bundles :: Govee Binding 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 index 72cfcbe174f10..c1074ff452c25 100644 --- 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 @@ -37,6 +37,9 @@ public class GoveeBindingConstants { // List of all Channel ids public static final String SWITCH = "switch"; public static final String COLOR = "color"; + public static final String COLOR_TEMPERATURE = "color-temperature"; + public static final int COLOR_TEMPERATURE_MIN_VALUE = 2000; + public static final int COLOR_TEMPERATURE_MAX_VALUE = 9000; public static final String COLOR_TEMPERATURE_ABS = "color-temperature-abs"; public static final String BRIGHTNESS = "brightness"; } 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 index 6afe221a372c3..2acc687691aaa 100644 --- 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 @@ -31,7 +31,7 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.govee.internal.model.DiscoveryMessage; +import org.openhab.binding.govee.internal.model.DiscoveryResponse; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; @@ -197,7 +197,7 @@ public Map getDeviceProperties(String response) { Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class); Gson gson = new Gson(); - DiscoveryMessage message = gson.fromJson(response, DiscoveryMessage.class); + DiscoveryResponse message = gson.fromJson(response, DiscoveryResponse.class); String ipAddress = ""; String sku = ""; String macAddress = ""; 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 index b1400515f0ea8..b222f6477cf66 100644 --- 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 @@ -14,7 +14,10 @@ import static org.openhab.binding.govee.internal.GoveeBindingConstants.BRIGHTNESS; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_ABS; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MAX_VALUE; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MIN_VALUE; import java.io.IOException; import java.net.DatagramPacket; @@ -32,9 +35,9 @@ 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.GenericGoveeMessage; import org.openhab.binding.govee.internal.model.GenericGoveeMsg; -import org.openhab.binding.govee.internal.model.StatusMessage; +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.binding.govee.internal.model.ValueStringData; import org.openhab.core.common.ThreadPoolManager; @@ -155,7 +158,7 @@ public static boolean isRefreshJobRunning() { if (!response.isEmpty()) { try { - StatusMessage statusMessage = GSON.fromJson(response, StatusMessage.class); + StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class); thingHandler.updateDeviceState(statusMessage); } catch (JsonSyntaxException jse) { thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -277,30 +280,39 @@ public void handleCommand(ChannelUID channelUID, Command command) { case COLOR: if (command instanceof HSBType hsbCommand) { int[] rgb = ColorUtil.hsbToRgb(hsbCommand); - GenericGoveeMsg lightColor = new GenericGoveeMsg("colorwc", - new ColorData(new Color(rgb[0], rgb[1], rgb[2]), 0)); + GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", + new ColorData(new Color(rgb[0], rgb[1], rgb[2]), 0))); + send(GSON.toJson(lightColor)); + } + break; + case COLOR_TEMPERATURE: + if (command instanceof PercentType percent) { + Double brightness = COLOR_TEMPERATURE_MIN_VALUE + percent.floatValue() + * (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0; + GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", + new ColorData(new Color(0, 0, 0), brightness.intValue()))); send(GSON.toJson(lightColor)); } break; case COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { - GenericGoveeMsg lightColor = new GenericGoveeMsg("colorwc", - new ColorData(new Color(0, 0, 0), quantity.intValue())); + GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", + new ColorData(new Color(0, 0, 0), quantity.intValue()))); send(GSON.toJson(lightColor)); } break; case BRIGHTNESS: if (command instanceof PercentType percent) { - GenericGoveeMessage lightBrightness = new GenericGoveeMessage( + GenericGoveeRequest lightBrightness = new GenericGoveeRequest( new GenericGoveeMsg("brightness", new ValueIntData(percent.intValue()))); send(GSON.toJson(lightBrightness)); } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { - GenericGoveeMessage lightOn = new GenericGoveeMessage( + GenericGoveeRequest lightOn = new GenericGoveeRequest( new GenericGoveeMsg("turn", new ValueStringData("on"))); send(GSON.toJson(lightOn)); } else { - GenericGoveeMessage lightOff = new GenericGoveeMessage( + GenericGoveeRequest lightOff = new GenericGoveeRequest( new GenericGoveeMsg("turn", new ValueStringData("off"))); send(GSON.toJson(lightOff)); } @@ -335,7 +347,7 @@ private void triggerDeviceStatusRefresh() throws IOException { LOGGER.debug("trigger Refresh Status of device {}", thing.getLabel()); try { - GenericGoveeMessage lightQuery = new GenericGoveeMessage( + GenericGoveeRequest lightQuery = new GenericGoveeRequest( new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); send(GSON.toJson(lightQuery)); } finally { @@ -356,7 +368,7 @@ public void send(String message) throws IOException { socket.close(); } - public void updateDeviceState(@Nullable StatusMessage message) { + public void updateDeviceState(@Nullable StatusResponse message) { if (message == null) { return; } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java similarity index 88% rename from bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java index 1a40cb458b680..0ddb2167907eb 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java @@ -22,8 +22,8 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public record DiscoveryMessage(DiscoveryMsg msg) { - public DiscoveryMessage() { +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/GenericGoveeMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java similarity index 91% rename from bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java index 7d04b1b15b892..d159b01983708 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java @@ -22,5 +22,5 @@ * @author Stefan Höhn - Initial contribution */ @NonNullByDefault -public record GenericGoveeMessage(GenericGoveeMsg msg) { +public record GenericGoveeRequest(GenericGoveeMsg msg) { } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java similarity index 91% rename from bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java rename to bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java index 94a577e5c7310..2b75a787db83d 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMessage.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java @@ -19,5 +19,5 @@ * * @author Stefan Höhn - Initial contribution */ -public record StatusMessage(StatusMsg msg) { +public record StatusResponse(StatusMsg msg) { } 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 index 2e9b4a337b391..a69d9f86f765b 100644 --- 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 @@ -11,6 +11,7 @@ + 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 index 1c8cee30ae97c..1aededf83e7f6 100644 --- 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 @@ -19,8 +19,8 @@ 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.GenericGoveeMessage; 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; @@ -40,17 +40,17 @@ public class GoveeSerializeTest { @Test public void testSerializeMessage() { - GenericGoveeMessage lightOff = new GenericGoveeMessage(new GenericGoveeMsg("turn", new ValueIntData(0))); + GenericGoveeRequest lightOff = new GenericGoveeRequest(new GenericGoveeMsg("turn", new ValueIntData(0))); assertEquals(lightOffJsonString, GSON.toJson(lightOff)); - GenericGoveeMessage lightOn = new GenericGoveeMessage(new GenericGoveeMsg("brightness", new ValueIntData(100))); + GenericGoveeRequest lightOn = new GenericGoveeRequest(new GenericGoveeMsg("brightness", new ValueIntData(100))); assertEquals(lightOnJsonString, GSON.toJson(lightOn)); - GenericGoveeMessage lightColor = new GenericGoveeMessage( + GenericGoveeRequest lightColor = new GenericGoveeRequest( new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 1, 2), 3))); assertEquals(lightColorJsonString, GSON.toJson(lightColor)); - GenericGoveeMessage lightBrightness = new GenericGoveeMessage( + GenericGoveeRequest lightBrightness = new GenericGoveeRequest( new GenericGoveeMsg("brightness", new ValueIntData(99))); assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness)); - GenericGoveeMessage lightQuery = new GenericGoveeMessage( + GenericGoveeRequest lightQuery = new GenericGoveeRequest( new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); assertEquals(lightQueryJsonString, GSON.toJson(lightQuery)); } From 6aa51c884d09e6e595187cf5c331033aa685f61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Wed, 1 Nov 2023 20:21:19 +0100 Subject: [PATCH 15/29] Refactor thread, rework on rest of feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 20 ++- bundles/org.openhab.binding.govee/pom.xml | 15 --- .../govee/internal/GoveeDiscoveryService.java | 31 ++--- .../binding/govee/internal/GoveeHandler.java | 117 +++++------------- .../govee/internal/RefreshStatusReceiver.java | 107 ++++++++++++++++ .../govee/internal/model/DiscoveryData.java | 6 +- .../internal/model/DiscoveryResponse.java | 3 +- .../internal/model/GenericGoveeRequest.java | 3 +- .../govee/internal/model/StatusData.java | 6 +- .../govee/internal/model/StatusResponse.java | 3 +- .../resources/OH-INF/thing/thing-types.xml | 6 +- 11 files changed, 176 insertions(+), 141 deletions(-) create mode 100644 bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index fe10eee21bc19..2343de069fa6f 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -23,11 +23,11 @@ Here is a list of the supported devices (the ones marked with * have been tested - H619Z RGBIC Pro LED Strip Lights - H6046 RGBIC TV Light Bars - H6047 RGBIC Gaming Light Bars with Smart Controller -- H6061 Glide Hexa LED Panels +- 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 +- H6067 Glide Triangle Light Panels (*) - H6072 RGBICWW Corner Floor Lamp - H6076 RGBICW Smart Corner Floor Lamp (*) - H6073 LED Floor Lamp @@ -111,17 +111,15 @@ arp -a | grep "MAC_ADDRESS" | 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 | Read/Write | Description | -|-----------------------|-------------------|------------|---------------------| -| brightness | Percentage | RW | | -| | Switch | RW | Power On / OFF | -| color | Color HSB Type | RW | | -| color-temperature | Percentage | RW | | -| color-temperature-abs | Color Temperature | RW | in 2000-9000 Kelvin | - +| Channel | Type | Read/Write | Description | +|-----------------------|------------------------------|------------|----------------------| +| brightness | Percentage | RW | | +| | Switch | RW | Power On / OFF | +| color | Color HSB Type | RW | | +| color-temperature | Color Temperature Percentage | RW | | +| color-temperature-abs | Color Temperature Absolute | RW | in 2000-9000 Kelvin | Note: you may have to add "%.0f K" as the state description when creating a color-temperature-abs item. diff --git a/bundles/org.openhab.binding.govee/pom.xml b/bundles/org.openhab.binding.govee/pom.xml index 00bef3961d934..6a7245634acb3 100644 --- a/bundles/org.openhab.binding.govee/pom.xml +++ b/bundles/org.openhab.binding.govee/pom.xml @@ -10,21 +10,6 @@ 4.1.0-SNAPSHOT - - - org.openhab.binding.govee openHAB Add-ons :: Bundles :: Govee Binding 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 index 2acc687691aaa..771b09ce76b8d 100644 --- 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 @@ -60,20 +60,23 @@ * * 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"
- * }
+ *   "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. * @@ -142,7 +145,7 @@ protected void startScan() { receiveSocket.setReuseAddress(true); sendBroadcastToDiscoverThing(sendSocket, receiveSocket, broadcastAddress); } catch (IOException e) { - logger.warn("Discovery with IO exception: {}", e.getMessage()); + logger.debug("Discovery with IO exception: {}", e.getMessage()); } }); } catch (InterruptedException ie) { @@ -190,7 +193,7 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); thingDiscovered(discoveryResult.build()); - } while (true); // left by SocketTimeoutException + } while (Thread.currentThread().isInterrupted()); // left by SocketTimeoutException } public Map getDeviceProperties(String response) { @@ -254,7 +257,7 @@ private List getLocalNetworkInterfaces() { } } } catch (SocketException exception) { - return Collections.emptyList(); + 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 index b222f6477cf66..a4677ecfc80bf 100644 --- 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 @@ -23,8 +23,6 @@ import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; -import java.net.MulticastSocket; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ScheduledFuture; @@ -39,7 +37,6 @@ 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.binding.govee.internal.model.ValueStringData; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; @@ -58,7 +55,6 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; /** * The {@link GoveeHandler} is responsible for handling commands, which are @@ -91,14 +87,14 @@ public class GoveeHandler extends BaseThingHandler { private static final Gson GSON = new Gson(); // Holds a list of all thing handlers to send them thing updates via the receiver-Thread - private static final Map THING_HANDLERS = new HashMap<>(); + public static final Map THING_HANDLERS = new HashMap<>(); - private final Logger LOGGER = LoggerFactory.getLogger(GoveeHandler.class); + private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class); private static final int SENDTODEVICE_PORT = 4003; - private static final int RECEIVEFROMDEVICE_PORT = 4002; + public static final int RECEIVEFROMDEVICE_PORT = 4002; // Semaphores to suppress further processing if already running - private static boolean refreshJobRunning = false; + public static boolean refreshJobRunning = false; private static boolean refreshRunning = false; @Nullable @@ -115,74 +111,6 @@ public static boolean isRefreshJobRunning() { return refreshJobRunning && THING_HANDLERS.isEmpty(); } - private static final Runnable REFRESH_STATUS_RECEIVER = () -> { - final Logger LOGGER = LoggerFactory.getLogger(GoveeHandler.class); - /* - * This thread receives an answer from any device. - * Therefore it needs to apply it to the right thing - * - * Discovery uses the same response code, so we must not refresh the status during discovery - */ - if (GoveeDiscoveryService.isDiscoveryActive()) { - LOGGER.debug("Not running refresh as Scan is currently active"); - } - - refreshJobRunning = true; - LOGGER.trace("REFRESH: running refresh cycle for {} devices", THING_HANDLERS.size()); - - if (THING_HANDLERS.isEmpty()) { - return; - } - - GoveeHandler thingHandler; - - try (MulticastSocket socket = new MulticastSocket(RECEIVEFROMDEVICE_PORT)) { - byte[] buffer = new byte[10240]; - DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket.setReuseAddress(true); - LOGGER.debug("waiting for Status"); - socket.receive(packet); - - String response = new String(packet.getData()).trim(); - String deviceIPAddress = packet.getAddress().toString().replace("/", ""); - LOGGER.trace("received = {} from {}", response, deviceIPAddress); - - thingHandler = THING_HANDLERS.get(deviceIPAddress); - if (thingHandler == null) { - LOGGER.warn("thing Handler for {} couldn't be found.", deviceIPAddress); - return; - } - - LOGGER.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); - LOGGER.trace("Response from {} = {}", deviceIPAddress, response); - - if (!response.isEmpty()) { - try { - StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class); - thingHandler.updateDeviceState(statusMessage); - } catch (JsonSyntaxException jse) { - thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - jse.getMessage()); - } - } else { - thingHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.communication-error.empty-response"); - } - if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { - thingHandler.updateStatus(ThingStatus.ONLINE); - } - } catch (IOException e) { - LOGGER.error("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); - // 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 { - refreshJobRunning = false; - } - }; - /** * 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 @@ -221,14 +149,13 @@ public void initialize() { startRefreshStatusJob(); } + updateStatus(ThingStatus.UNKNOWN); if (triggerStatusJob == null) { - LOGGER.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); + logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100, goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS); } - - updateStatus(ThingStatus.UNKNOWN); } /** @@ -245,10 +172,10 @@ public static void stopRefreshStatusJob() { /** * (re)start the refresh status job */ - public static void startRefreshStatusJob() { + public static synchronized void startRefreshStatusJob() { if (refreshStatusJob == null) { refreshStatusJob = ThreadPoolManager.getScheduledPool("goveeThingHandler") - .scheduleWithFixedDelay(REFRESH_STATUS_RECEIVER, 100, 1000, TimeUnit.MILLISECONDS); + .scheduleWithFixedDelay(new RefreshStatusReceiver(), 100, 1000, TimeUnit.MILLISECONDS); } } @@ -309,11 +236,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { GenericGoveeRequest lightOn = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueStringData("on"))); + new GenericGoveeMsg("turn", new ValueIntData(1))); send(GSON.toJson(lightOn)); } else { GenericGoveeRequest lightOff = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueStringData("off"))); + new GenericGoveeMsg("turn", new ValueIntData(0))); send(GSON.toJson(lightOff)); } } @@ -339,12 +266,12 @@ private void triggerDeviceStatusRefresh() throws IOException { return; } if (GoveeDiscoveryService.isDiscoveryActive()) { - LOGGER.debug("Not triggering refresh as Scan is currently active"); + logger.debug("Not triggering refresh as Scan is currently active"); return; } refreshRunning = true; - LOGGER.debug("trigger Refresh Status of device {}", thing.getLabel()); + logger.debug("trigger Refresh Status of device {}", thing.getLabel()); try { GenericGoveeRequest lightQuery = new GenericGoveeRequest( @@ -362,7 +289,7 @@ public void send(String message) throws IOException { byte[] data = message.getBytes(); InetAddress address = InetAddress.getByName(goveeConfiguration.hostname); - LOGGER.trace("Sending {} to {}", message, goveeConfiguration.hostname); + logger.trace("Sending {} to {}", message, goveeConfiguration.hostname); DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); socket.send(packet); socket.close(); @@ -380,9 +307,21 @@ public void updateDeviceState(@Nullable StatusResponse message) { updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); - LOGGER.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); + logger.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); updateState(BRIGHTNESS, OnOffType.from(lastOnOff == 1)); - LOGGER.debug("setting BRIGHTNESS to PercentType {}", new PercentType(lastBrightness)); - updateState(BRIGHTNESS, new PercentType(lastBrightness)); + if (lastOnOff == 1) { + logger.debug("setting BRIGHTNESS to PercentType {}", new PercentType(lastBrightness)); + updateState(BRIGHTNESS, new PercentType(lastBrightness)); + } else { + logger.debug("not updating BRIGHTNESS percentage as device is OFF (would turn channel switch on)"); + } + } + + public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + updateStatus(status, statusDetail, description); + } + + public void statusUpdate(ThingStatus status) { + updateStatus(status); } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java new file mode 100644 index 0000000000000..db61b024dcbe3 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java @@ -0,0 +1,107 @@ +/** + * 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.MulticastSocket; +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.govee.internal.model.StatusResponse; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link RefreshStatusReceiver} is a thread that handles the answers of all devices + * Therefore it needs to apply the information it 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 + */ +@NonNullByDefault +public class RefreshStatusReceiver implements Runnable { + final Logger logger = LoggerFactory.getLogger(RefreshStatusReceiver.class); + private static final Gson GSON = new Gson(); + + public RefreshStatusReceiver() { + } + + @Override + public void run() { + if (GoveeDiscoveryService.isDiscoveryActive()) { + logger.debug("Not running refresh as Scan is currently active"); + } + + GoveeHandler.refreshJobRunning = true; + logger.trace("REFRESH: running refresh cycle for {} devices", GoveeHandler.THING_HANDLERS.size()); + + if (GoveeHandler.THING_HANDLERS.isEmpty()) { + return; + } + + GoveeHandler thingHandler; + + try (MulticastSocket socket = new MulticastSocket(GoveeHandler.RECEIVEFROMDEVICE_PORT)) { + byte[] buffer = new byte[10240]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + socket.setReuseAddress(true); + logger.debug("waiting for Status"); + socket.receive(packet); + + String response = new String(packet.getData()).trim(); + String deviceIPAddress = packet.getAddress().toString().replace("/", ""); + logger.trace("received = {} from {}", response, deviceIPAddress); + + thingHandler = GoveeHandler.THING_HANDLERS.get(deviceIPAddress); + if (thingHandler == null) { + logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress); + return; + } + + logger.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); + logger.trace("Response from {} = {}", deviceIPAddress, response); + + if (!response.isEmpty()) { + try { + StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class); + thingHandler.updateDeviceState(statusMessage); + } catch (JsonSyntaxException jse) { + thingHandler.statusUpdate(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + jse.getMessage()); + } + } else { + thingHandler.statusUpdate(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.empty-response"); + } + if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { + thingHandler.statusUpdate(ThingStatus.ONLINE); + } + } catch (IOException e) { + logger.error("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); + // 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 { + GoveeHandler.refreshJobRunning = false; + } + } +} 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 index 04b7b90d643dd..24d0411dfa47b 100644 --- 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 @@ -18,9 +18,9 @@ /** * Govee Message - Device information * - * @param ip IP Address of the device - * @param device Mac Address - * @param sku artice number + * @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 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 index 0ddb2167907eb..a1b7ae5b8cec8 100644 --- 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 @@ -17,7 +17,8 @@ /** * Govee Message - * @ param msg + * + * @param msg message block * * @author Stefan Höhn - Initial contribution */ 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 index d159b01983708..f8bb47b945fee 100644 --- 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 @@ -17,7 +17,8 @@ /** * Govee Message - * @ param msg + * + * @param msg message block * * @author Stefan Höhn - Initial contribution */ 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 index 5d353739a8888..33941a2e6b33c 100644 --- 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 @@ -17,10 +17,10 @@ * * @param onOff on=1 off=0 * @param brightness brightness - * @param color rgb Color - * @param colorTemInKelvin color in Kelvin + * @param color rgb color + * @param colorTemInKelvin color temperature in Kelvin * - * * @author Stefan Höhn - Initial contribution + * @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/StatusResponse.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java index 2b75a787db83d..19286ec5033c9 100644 --- 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 @@ -15,7 +15,8 @@ /** * Govee Message - * @ param msg + * + * @param msg message block * * @author Stefan Höhn - Initial contribution */ 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 index a69d9f86f765b..a35c4967e5ed2 100644 --- 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 @@ -5,19 +5,19 @@ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - + Govee light controllable via LAN API - + - + Number:Temperature Controls the color temperature of the light in Kelvin From 0e499d9454a0308a949a6f1e58c850ef05494913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 4 Nov 2023 13:10:26 +0100 Subject: [PATCH 16/29] fix thread condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../openhab/binding/govee/internal/GoveeDiscoveryService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 771b09ce76b8d..e86187fbd4473 100644 --- 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 @@ -193,7 +193,7 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); thingDiscovered(discoveryResult.build()); - } while (Thread.currentThread().isInterrupted()); // left by SocketTimeoutException + } while (!Thread.currentThread().isInterrupted()); // left by SocketTimeoutException } public Map getDeviceProperties(String response) { From 5760d0218477d2ab99f7660b6e03f08fd86449dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 18 Nov 2023 16:58:58 +0100 Subject: [PATCH 17/29] WIP: Non-Working tripple channel-id support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 14 ++--- .../govee/internal/GoveeBindingConstants.java | 1 - .../binding/govee/internal/GoveeHandler.java | 61 +++++++++++-------- .../govee/internal/RefreshStatusReceiver.java | 2 +- .../resources/OH-INF/thing/thing-types.xml | 1 - 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index 2343de069fa6f..797673a2baa0f 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -113,13 +113,13 @@ arp -a | grep "MAC_ADDRESS" ## Channels -| Channel | Type | Read/Write | Description | -|-----------------------|------------------------------|------------|----------------------| -| brightness | Percentage | RW | | -| | Switch | RW | Power On / OFF | -| color | Color HSB Type | RW | | -| color-temperature | Color Temperature Percentage | RW | | -| color-temperature-abs | Color Temperature Absolute | RW | in 2000-9000 Kelvin | +| 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 have to add "%.0f K" as the state description when creating a color-temperature-abs item. 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 index c1074ff452c25..abfa97619fac4 100644 --- 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 @@ -41,5 +41,4 @@ public class GoveeBindingConstants { public static final int COLOR_TEMPERATURE_MIN_VALUE = 2000; public static final int COLOR_TEMPERATURE_MAX_VALUE = 9000; public static final String COLOR_TEMPERATURE_ABS = "color-temperature-abs"; - public static final String BRIGHTNESS = "brightness"; } 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 index a4677ecfc80bf..8181e98afac6f 100644 --- 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 @@ -12,7 +12,6 @@ */ package org.openhab.binding.govee.internal; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.BRIGHTNESS; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_ABS; @@ -103,6 +102,7 @@ public class GoveeHandler extends BaseThingHandler { private ScheduledFuture triggerStatusJob; // send device status update job private GoveeConfiguration goveeConfiguration = new GoveeConfiguration(); private int lastBrightness; + private Color lastColor = new Color(0, 0, 0); /* * Common Receiver job for the status answers of the devices @@ -211,6 +211,21 @@ public void handleCommand(ChannelUID channelUID, Command command) { new ColorData(new Color(rgb[0], rgb[1], rgb[2]), 0))); send(GSON.toJson(lightColor)); } + if (command instanceof PercentType percent) { + GenericGoveeRequest lightBrightness = new GenericGoveeRequest( + new GenericGoveeMsg("brightness", new ValueIntData(percent.intValue()))); + send(GSON.toJson(lightBrightness)); + } else if (command instanceof OnOffType) { + if (command.equals(OnOffType.ON)) { + GenericGoveeRequest lightOn = new GenericGoveeRequest( + new GenericGoveeMsg("turn", new ValueIntData(1))); + send(GSON.toJson(lightOn)); + } else { + GenericGoveeRequest lightOff = new GenericGoveeRequest( + new GenericGoveeMsg("turn", new ValueIntData(0))); + send(GSON.toJson(lightOff)); + } + } break; case COLOR_TEMPERATURE: if (command instanceof PercentType percent) { @@ -228,23 +243,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { send(GSON.toJson(lightColor)); } break; - case BRIGHTNESS: - if (command instanceof PercentType percent) { - GenericGoveeRequest lightBrightness = new GenericGoveeRequest( - new GenericGoveeMsg("brightness", new ValueIntData(percent.intValue()))); - send(GSON.toJson(lightBrightness)); - } else if (command instanceof OnOffType) { - if (command.equals(OnOffType.ON)) { - GenericGoveeRequest lightOn = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueIntData(1))); - send(GSON.toJson(lightOn)); - } else { - GenericGoveeRequest lightOff = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueIntData(0))); - send(GSON.toJson(lightOff)); - } - } - break; } } if (!thing.getStatus().equals(ThingStatus.ONLINE)) { @@ -300,21 +298,32 @@ public void updateDeviceState(@Nullable StatusResponse message) { return; } + logger.info("Update Device State ----------------------------------------------"); int lastOnOff = message.msg().data().onOff(); + logger.info("lastOnOff = {}", lastOnOff); lastBrightness = message.msg().data().brightness(); - Color lastColor = message.msg().data().color(); + logger.info("lastbrigthess = {}", lastBrightness); + lastColor = message.msg().data().color(); + logger.info("lastColor = {}", lastColor); + int lastColorTemperature = message.msg().data().colorTemInKelvin(); - updateState(COLOR, ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() })); - updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); - logger.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); - updateState(BRIGHTNESS, OnOffType.from(lastOnOff == 1)); + logger.info("Last RGB = {} {} {}", lastColor.r(), lastColor.g(), lastColor.b()); + HSBType hsbColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() }); + logger.info("Last HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); + if (lastOnOff == 1) { - logger.debug("setting BRIGHTNESS to PercentType {}", new PercentType(lastBrightness)); - updateState(BRIGHTNESS, new PercentType(lastBrightness)); + logger.info("setting BRIGHTNESS on Color to be consistent by using lastBrightness = {}", lastBrightness); + hsbColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastBrightness }); } else { - logger.debug("not updating BRIGHTNESS percentage as device is OFF (would turn channel switch on)"); + logger.info("not updating BRIGHTNESS percentage as device is OFF (would turn channel switch on)"); } + + updateState(COLOR, hsbColor); + updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); + // FIXME : handle state + // updateState(COLOR_TEMPERATURE, new QuantityType(lastColorTemperature, Units.KELVIN)); + logger.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); } public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java index db61b024dcbe3..92433f50d94da 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java @@ -76,7 +76,7 @@ public void run() { } logger.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); - logger.trace("Response from {} = {}", deviceIPAddress, response); + logger.info("Response from {} = {}", deviceIPAddress, response); if (!response.isEmpty()) { try { 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 index a35c4967e5ed2..002cb291f70c4 100644 --- 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 @@ -9,7 +9,6 @@ Govee light controllable via LAN API - From f110a9a2e40a03ab0a5ecf096c92c1d856e116a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Wed, 22 Nov 2023 22:50:02 +0100 Subject: [PATCH 18/29] refactor channel handling with local caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../binding/govee/internal/GoveeHandler.java | 78 ++++++++++++------- 1 file changed, 50 insertions(+), 28 deletions(-) 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 index 8181e98afac6f..a3b0765eaba6b 100644 --- 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 @@ -101,8 +101,12 @@ public class GoveeHandler extends BaseThingHandler { @Nullable private ScheduledFuture triggerStatusJob; // send device status update job private GoveeConfiguration goveeConfiguration = new GoveeConfiguration(); + + private int lastOnOff; private int lastBrightness; private Color lastColor = new Color(0, 0, 0); + private int lastColorTempInPercent = 0; + private int lastColorTempInKelvin = 0; /* * Common Receiver job for the status answers of the devices @@ -207,40 +211,31 @@ public void handleCommand(ChannelUID channelUID, Command command) { case COLOR: if (command instanceof HSBType hsbCommand) { int[] rgb = ColorUtil.hsbToRgb(hsbCommand); - GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", - new ColorData(new Color(rgb[0], rgb[1], rgb[2]), 0))); - send(GSON.toJson(lightColor)); + sendColor(new Color(rgb[0], rgb[1], rgb[2])); } if (command instanceof PercentType percent) { - GenericGoveeRequest lightBrightness = new GenericGoveeRequest( - new GenericGoveeMsg("brightness", new ValueIntData(percent.intValue()))); - send(GSON.toJson(lightBrightness)); + sendBrightness(percent.intValue()); } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { - GenericGoveeRequest lightOn = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueIntData(1))); - send(GSON.toJson(lightOn)); + sendOnOff(OnOffType.ON); + } else { - GenericGoveeRequest lightOff = new GenericGoveeRequest( - new GenericGoveeMsg("turn", new ValueIntData(0))); - send(GSON.toJson(lightOff)); + sendOnOff(OnOffType.OFF); } } break; case COLOR_TEMPERATURE: if (command instanceof PercentType percent) { - Double brightness = COLOR_TEMPERATURE_MIN_VALUE + percent.floatValue() - * (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0; - GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", - new ColorData(new Color(0, 0, 0), brightness.intValue()))); - send(GSON.toJson(lightColor)); + Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.floatValue() + * (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0); + lastColorTempInKelvin = colorTemp.intValue(); + sendColorTemp(lastColorTempInKelvin); } break; case COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { - GenericGoveeRequest lightColor = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", - new ColorData(new Color(0, 0, 0), quantity.intValue()))); - send(GSON.toJson(lightColor)); + lastColorTempInPercent = quantity.intValue(); + sendColorTemp(lastColorTempInPercent); } break; } @@ -280,6 +275,34 @@ private void triggerDeviceStatusRefresh() throws IOException { } } + public void sendColor(Color color) throws IOException { + lastColor = color; + GenericGoveeRequest lightColor = new GenericGoveeRequest( + new GenericGoveeMsg("colorwc", new ColorData(color, lastColorTempInKelvin))); + send(GSON.toJson(lightColor)); + } + + public void sendBrightness(int brightness) throws IOException { + lastBrightness = brightness; + GenericGoveeRequest lightBrightness = new GenericGoveeRequest( + new GenericGoveeMsg("brightness", new ValueIntData(brightness))); + send(GSON.toJson(lightBrightness)); + } + + private void sendOnOff(OnOffType switchValue) throws IOException { + lastOnOff = (switchValue == OnOffType.ON) ? 1 : 0; + GenericGoveeRequest switchLight = new GenericGoveeRequest( + new GenericGoveeMsg("turn", new ValueIntData(lastOnOff))); + send(GSON.toJson(switchLight)); + } + + private void sendColorTemp(int colorTemp) throws IOException { + lastColorTempInKelvin = colorTemp; + GenericGoveeRequest lightColor = new GenericGoveeRequest( + new GenericGoveeMsg("colorwc", new ColorData(lastColor, colorTemp))); + send(GSON.toJson(lightColor)); + } + public void send(String message) throws IOException { DatagramSocket socket; socket = new DatagramSocket(); @@ -299,31 +322,30 @@ public void updateDeviceState(@Nullable StatusResponse message) { } logger.info("Update Device State ----------------------------------------------"); - int lastOnOff = message.msg().data().onOff(); + lastOnOff = message.msg().data().onOff(); logger.info("lastOnOff = {}", lastOnOff); lastBrightness = message.msg().data().brightness(); logger.info("lastbrigthess = {}", lastBrightness); lastColor = message.msg().data().color(); logger.info("lastColor = {}", lastColor); + lastColorTempInKelvin = message.msg().data().colorTemInKelvin(); + logger.info("lastColorTempInKelvin = {}", lastColorTempInKelvin); int lastColorTemperature = message.msg().data().colorTemInKelvin(); - logger.info("Last RGB = {} {} {}", lastColor.r(), lastColor.g(), lastColor.b()); - HSBType hsbColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() }); + logger.info("Last RGB = {} {} {}, brightnes {}", lastColor.r(), lastColor.g(), lastColor.b(), lastBrightness); + HSBType hsbColor = HSBType.fromRGB(lastColor.r(), lastColor.g(), lastColor.b()); logger.info("Last HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); if (lastOnOff == 1) { logger.info("setting BRIGHTNESS on Color to be consistent by using lastBrightness = {}", lastBrightness); - hsbColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastBrightness }); } else { logger.info("not updating BRIGHTNESS percentage as device is OFF (would turn channel switch on)"); } updateState(COLOR, hsbColor); - updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTemperature, Units.KELVIN)); - // FIXME : handle state - // updateState(COLOR_TEMPERATURE, new QuantityType(lastColorTemperature, Units.KELVIN)); - logger.debug("setting BRIGHTNESS to ONOFF {}", OnOffType.from(lastOnOff == 1)); + updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); + updateState(COLOR_TEMPERATURE, new PercentType(lastColorTempInPercent)); } public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { From 04f4829ac2c6e5406660e1a71858394aa638be01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 25 Nov 2023 21:09:04 +0100 Subject: [PATCH 19/29] working channel implementation, add more thing props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 60 ++++++++++-- .../doc/channel-setup1.png | Bin 0 -> 45122 bytes .../doc/channel-setup2.png | Bin 0 -> 24210 bytes .../doc/channel-setup3.png | Bin 0 -> 28815 bytes .../doc/ui-example.png | Bin 0 -> 30658 bytes .../govee/internal/GoveeBindingConstants.java | 6 +- .../govee/internal/GoveeDiscoveryService.java | 6 ++ .../binding/govee/internal/GoveeHandler.java | 86 +++++++++++++----- .../govee/internal/RefreshStatusReceiver.java | 2 +- .../main/resources/OH-INF/config/config.xml | 12 ++- 10 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 bundles/org.openhab.binding.govee/doc/channel-setup1.png create mode 100644 bundles/org.openhab.binding.govee/doc/channel-setup2.png create mode 100644 bundles/org.openhab.binding.govee/doc/channel-setup3.png create mode 100644 bundles/org.openhab.binding.govee/doc/ui-example.png diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index 797673a2baa0f..df298ca5fe06b 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -80,23 +80,23 @@ Here is a list of the supported devices (the ones marked with * have been tested ## Discovery -Discovery is done by scanning the devices in the Thing section. +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 +- 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: +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" @@ -119,10 +119,50 @@ arp -a | grep "MAC_ADDRESS" | | 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 | +| color-temperature-abs | Dimmer | Color Temperature Absolute | RW | in 2000-9000 Kelvin | Note: you may have to add "%.0f K" as the state description when creating a color-temperature-abs item. +## UI Example for one device + +![ui-example.png](doc%2Fui-example.png) + +Thing channel setup: + +![channel-setup1.png](doc%2Fchannel-setup1.png) +![channel-setup2.png](doc%2Fchannel-setup2.png) +![channel-setup3.png](doc%2Fchannel-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 0000000000000000000000000000000000000000..de86255dac23bdc5bd9ab121114580ef83464b9c GIT binary patch literal 45122 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 D_*%{~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ab770d5502946d4111793b38702168684cc6ea5b GIT binary patch literal 24210 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 K9952Vi1{z#ajfM4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..52b4675793ae3f6177d4db6729160e991ba32fb8 GIT binary patch literal 28815 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 bOa^~IE-nZcC#7UoI@Hk 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 literal 0 HcmV?d00001 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 index abfa97619fac4..aa029f78c4ef4 100644 --- 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 @@ -29,6 +29,8 @@ public class GoveeBindingConstants { 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 @@ -38,7 +40,7 @@ public class GoveeBindingConstants { public static final String SWITCH = "switch"; public static final String COLOR = "color"; public static final String COLOR_TEMPERATURE = "color-temperature"; - public static final int COLOR_TEMPERATURE_MIN_VALUE = 2000; - public static final int COLOR_TEMPERATURE_MAX_VALUE = 9000; + public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0; + public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.0; public static final String COLOR_TEMPERATURE_ABS = "color-temperature-abs"; } 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 index e86187fbd4473..4fb7d3db6862b 100644 --- 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 @@ -205,11 +205,15 @@ public Map getDeviceProperties(String response) { String sku = ""; String macAddress = ""; String productName = ""; + String hwVersion = "unknown"; + String swVersion = "unknown"; if (message != null) { ipAddress = message.msg().data().ip(); sku = message.msg().data().sku(); macAddress = message.msg().data().device(); + hwVersion = message.msg().data().wifiVersionHard(); + swVersion = message.msg().data().wifiVersionSoft(); if (ipAddress.isEmpty()) { ipAddress = "unknown"; @@ -238,6 +242,8 @@ public Map getDeviceProperties(String response) { properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); + properties.put(GoveeBindingConstants.HW_VERSION, hwVersion); + properties.put(GoveeBindingConstants.SW_VERSION, swVersion); properties.put(GoveeBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); return properties; 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 index a3b0765eaba6b..771265e2cc661 100644 --- 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 @@ -27,6 +27,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.measure.quantity.Temperature; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.govee.internal.model.Color; @@ -75,6 +77,9 @@ * like 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 @@ -105,8 +110,7 @@ public class GoveeHandler extends BaseThingHandler { private int lastOnOff; private int lastBrightness; private Color lastColor = new Color(0, 0, 0); - private int lastColorTempInPercent = 0; - private int lastColorTempInKelvin = 0; + private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue(); /* * Common Receiver job for the status answers of the devices @@ -165,6 +169,7 @@ public void initialize() { /** * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) */ + @SuppressWarnings("null") public static void stopRefreshStatusJob() { if (refreshStatusJob != null) { refreshStatusJob.cancel(true); @@ -184,6 +189,7 @@ public static synchronized void startRefreshStatusJob() { } @Override + @SuppressWarnings("null") public void dispose() { super.dispose(); @@ -206,7 +212,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { // we are refreshing all channels at once, as we get all information at the same time triggerDeviceStatusRefresh(); + logger.debug("Triggering Refresh"); } else { + logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass()); switch (channelUID.getId()) { case COLOR: if (command instanceof HSBType hsbCommand) { @@ -218,7 +226,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else if (command instanceof OnOffType) { if (command.equals(OnOffType.ON)) { sendOnOff(OnOffType.ON); - } else { sendOnOff(OnOffType.OFF); } @@ -226,16 +233,25 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case COLOR_TEMPERATURE: if (command instanceof PercentType percent) { - Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.floatValue() + logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", + command.toString()); + 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 COLOR_TEMPERATURE_ABS: - if (command instanceof QuantityType quantity) { - lastColorTempInPercent = quantity.intValue(); - sendColorTemp(lastColorTempInPercent); + if (command instanceof QuantityType quantity) { + logger.debug("Color Temperature Absolute change with Percent Type {}", command.toString()); + 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; } @@ -278,7 +294,7 @@ private void triggerDeviceStatusRefresh() throws IOException { public void sendColor(Color color) throws IOException { lastColor = color; GenericGoveeRequest lightColor = new GenericGoveeRequest( - new GenericGoveeMsg("colorwc", new ColorData(color, lastColorTempInKelvin))); + new GenericGoveeMsg("colorwc", new ColorData(color, 0))); send(GSON.toJson(lightColor)); } @@ -298,8 +314,9 @@ private void sendOnOff(OnOffType switchValue) throws IOException { private void sendColorTemp(int colorTemp) throws IOException { lastColorTempInKelvin = colorTemp; + logger.debug("sendColorTemp {}", colorTemp); GenericGoveeRequest lightColor = new GenericGoveeRequest( - new GenericGoveeMsg("colorwc", new ColorData(lastColor, colorTemp))); + new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp))); send(GSON.toJson(lightColor)); } @@ -310,41 +327,60 @@ public void send(String message) throws IOException { byte[] data = message.getBytes(); InetAddress address = InetAddress.getByName(goveeConfiguration.hostname); - logger.trace("Sending {} to {}", message, goveeConfiguration.hostname); + logger.debug("Sending {} to {}", message, goveeConfiguration.hostname); DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); socket.send(packet); socket.close(); } + /** + * 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 getLastColorState() { + PercentType brightness = (lastOnOff == 0) ? new PercentType(0) : new PercentType(lastBrightness); + int rgb[] = { lastColor.r(), lastColor.g(), lastColor.b() }; + HSBType hsb = ColorUtil.rgbToHsb(rgb); + HSBType hsbState = new HSBType(hsb.getHue(), hsb.getSaturation(), brightness); + return hsbState; + } + public void updateDeviceState(@Nullable StatusResponse message) { if (message == null) { return; } - logger.info("Update Device State ----------------------------------------------"); + logger.debug("Update Device State ----------------------------------------------"); lastOnOff = message.msg().data().onOff(); - logger.info("lastOnOff = {}", lastOnOff); + logger.debug("lastOnOff = {}", lastOnOff); lastBrightness = message.msg().data().brightness(); - logger.info("lastbrigthess = {}", lastBrightness); + logger.debug("lastbrightness = {}", lastBrightness); lastColor = message.msg().data().color(); - logger.info("lastColor = {}", lastColor); + logger.debug("lastColor = {}", lastColor); lastColorTempInKelvin = message.msg().data().colorTemInKelvin(); - logger.info("lastColorTempInKelvin = {}", lastColorTempInKelvin); + logger.debug("lastColorTempInKelvin = {}", lastColorTempInKelvin); - int lastColorTemperature = message.msg().data().colorTemInKelvin(); + lastColorTempInKelvin = (lastColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE) + ? COLOR_TEMPERATURE_MIN_VALUE.intValue() + : lastColorTempInKelvin; + int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE) + / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue(); - logger.info("Last RGB = {} {} {}, brightnes {}", lastColor.r(), lastColor.g(), lastColor.b(), lastBrightness); - HSBType hsbColor = HSBType.fromRGB(lastColor.r(), lastColor.g(), lastColor.b()); - logger.info("Last HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); + logger.debug("Last RGB = {} {} {}, brightness {}", lastColor.r(), lastColor.g(), lastColor.b(), lastBrightness); - if (lastOnOff == 1) { - logger.info("setting BRIGHTNESS on Color to be consistent by using lastBrightness = {}", lastBrightness); - } else { - logger.info("not updating BRIGHTNESS percentage as device is OFF (would turn channel switch on)"); - } + HSBType hsbColor = getLastColorState(); + logger.debug("Send HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); updateState(COLOR, hsbColor); - updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); + logger.debug("Updating Color-Temperature Status: {} K {}%", lastColorTempInKelvin, lastColorTempInPercent); + updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); updateState(COLOR_TEMPERATURE, new PercentType(lastColorTempInPercent)); } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java index 92433f50d94da..52f1c697bfbe0 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java @@ -67,6 +67,7 @@ public void run() { String response = new String(packet.getData()).trim(); String deviceIPAddress = packet.getAddress().toString().replace("/", ""); + logger.trace("Response from {} = {}", deviceIPAddress, response); logger.trace("received = {} from {}", response, deviceIPAddress); thingHandler = GoveeHandler.THING_HANDLERS.get(deviceIPAddress); @@ -76,7 +77,6 @@ public void run() { } logger.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); - logger.info("Response from {} = {}", deviceIPAddress, response); if (!response.isEmpty()) { try { 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 index 7f339eb8d5fe9..8f832f4675809 100644 --- 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 @@ -15,9 +15,17 @@ MAC Address of the device - + The product number of the device + + + The Hardware Version of the device + + + + The Software Version of the device + Description of the device @@ -25,7 +33,7 @@ The amount of time that passes until the device is refreshed (in seconds) - 5 + 2 From a4400d3a6e08933ad64b1cdc1f2053ab7b6e42b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 25 Nov 2023 21:13:30 +0100 Subject: [PATCH 20/29] fix Readme image links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index df298ca5fe06b..6239985b949f2 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -125,13 +125,13 @@ Note: you may have to add "%.0f K" as the state description when creating a colo ## UI Example for one device -![ui-example.png](doc%2Fui-example.png) +![ui-example.png](doc/ui-example.png) Thing channel setup: -![channel-setup1.png](doc%2Fchannel-setup1.png) -![channel-setup2.png](doc%2Fchannel-setup2.png) -![channel-setup3.png](doc%2Fchannel-setup3.png) +![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 From dcc8e884219a5828b8db7971a36b2c9877701b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 26 Nov 2023 12:37:04 +0100 Subject: [PATCH 21/29] Christmas String Lights confirmed to work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index 6239985b949f2..ec0e847f4baea 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -71,6 +71,8 @@ Here is a list of the supported devices (the ones marked with * have been tested - 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 From 01561c918540c72fe09f67ab2e1f22eca9776fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 26 Nov 2023 14:10:39 +0100 Subject: [PATCH 22/29] review changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../govee/internal/GoveeBindingConstants.java | 9 ++--- .../govee/internal/GoveeDiscoveryService.java | 26 +++++++-------- .../binding/govee/internal/GoveeHandler.java | 33 ++++++++----------- .../resources/OH-INF/i18n/govee.properties | 4 +-- 4 files changed, 33 insertions(+), 39 deletions(-) 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 index aa029f78c4ef4..280a08adcce29 100644 --- 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 @@ -37,10 +37,11 @@ public class GoveeBindingConstants { public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light"); // List of all Channel ids - public static final String SWITCH = "switch"; - public static final String COLOR = "color"; - public static final String COLOR_TEMPERATURE = "color-temperature"; + 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; - public static final String COLOR_TEMPERATURE_ABS = "color-temperature-abs"; } 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 index 4fb7d3db6862b..aa10b80bb54de 100644 --- 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 @@ -103,7 +103,7 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT); @Activate - public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider) throws IllegalArgumentException { + public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider) { super(SUPPORTED_THING_TYPES_UIDS, 0, false); this.i18nProvider = i18nProvider; } @@ -149,9 +149,9 @@ protected void startScan() { } }); } catch (InterruptedException ie) { - // don't care + Thread.currentThread().interrupt(); } catch (UnknownHostException e) { - logger.warn("Discovery with UnknownHostException exception: {}", e.getMessage()); + logger.debug("Discovery with UnknownHostException exception: {}", e.getMessage()); } finally { discoveryActive = false; } @@ -181,9 +181,9 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS final Map properties = getDeviceProperties(response); final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); - final String productName = (product != null) ? product.toString() : "unknown"; + final String productName = product != null ? product.toString() : "???"; final Object mac = properties.get(GoveeBindingConstants.MAC_ADDRESS); - final String macAddress = (mac != null) ? mac.toString() : "unknown"; + final String macAddress = mac != null ? mac.toString() : "???"; ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_")); @@ -205,8 +205,8 @@ public Map getDeviceProperties(String response) { String sku = ""; String macAddress = ""; String productName = ""; - String hwVersion = "unknown"; - String swVersion = "unknown"; + String hwVersion = "???"; + String swVersion = "???"; if (message != null) { ipAddress = message.msg().data().ip(); @@ -216,29 +216,29 @@ public Map getDeviceProperties(String response) { swVersion = message.msg().data().wifiVersionSoft(); if (ipAddress.isEmpty()) { - ipAddress = "unknown"; + ipAddress = "???"; logger.warn("Empty IP Address received during discovery - device may not work"); } - productName = "unknown"; + productName = "???"; if (!sku.isEmpty()) { final String skuLabel = "discovery.govee-light." + sku; if (bundle != null) { productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); } } else { - sku = "unknown"; - productName = "unknown"; + sku = "???"; + productName = "???"; logger.warn("Empty SKU (product name) received during discovery - device may not work"); } if (macAddress.isEmpty()) { - macAddress = "unknown"; + macAddress = "???"; logger.warn("Empty Mac Address received during discovery - device may not work"); } } - Map properties = new HashMap<>(3); + Map properties = new HashMap<>(6); properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); 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 index 771265e2cc661..32553231dc189 100644 --- 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 @@ -12,9 +12,9 @@ */ package org.openhab.binding.govee.internal; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_ABS; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MAX_VALUE; import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MIN_VALUE; @@ -169,7 +169,6 @@ public void initialize() { /** * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) */ - @SuppressWarnings("null") public static void stopRefreshStatusJob() { if (refreshStatusJob != null) { refreshStatusJob.cancel(true); @@ -189,7 +188,6 @@ public static synchronized void startRefreshStatusJob() { } @Override - @SuppressWarnings("null") public void dispose() { super.dispose(); @@ -216,22 +214,17 @@ public void handleCommand(ChannelUID channelUID, Command command) { } else { logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass()); switch (channelUID.getId()) { - case COLOR: + case CHANNEL_COLOR: if (command instanceof HSBType hsbCommand) { int[] rgb = ColorUtil.hsbToRgb(hsbCommand); sendColor(new Color(rgb[0], rgb[1], rgb[2])); - } - if (command instanceof PercentType percent) { + } else if (command instanceof PercentType percent) { sendBrightness(percent.intValue()); - } else if (command instanceof OnOffType) { - if (command.equals(OnOffType.ON)) { - sendOnOff(OnOffType.ON); - } else { - sendOnOff(OnOffType.OFF); - } + } else if (command instanceof OnOffType onOffCommand) { + sendOnOff(onOffCommand); } break; - case COLOR_TEMPERATURE: + case CHANNEL_COLOR_TEMPERATURE: if (command instanceof PercentType percent) { logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", command.toString()); @@ -242,7 +235,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { sendColorTemp(lastColorTempInKelvin); } break; - case COLOR_TEMPERATURE_ABS: + case CHANNEL_COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { logger.debug("Color Temperature Absolute change with Percent Type {}", command.toString()); lastColorTempInKelvin = quantity.intValue(); @@ -345,7 +338,7 @@ public void send(String message) throws IOException { * @return the computed state */ private HSBType getLastColorState() { - PercentType brightness = (lastOnOff == 0) ? new PercentType(0) : new PercentType(lastBrightness); + PercentType brightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(lastBrightness); int rgb[] = { lastColor.r(), lastColor.g(), lastColor.b() }; HSBType hsb = ColorUtil.rgbToHsb(rgb); HSBType hsbState = new HSBType(hsb.getHue(), hsb.getSaturation(), brightness); @@ -378,10 +371,10 @@ public void updateDeviceState(@Nullable StatusResponse message) { HSBType hsbColor = getLastColorState(); logger.debug("Send HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); - updateState(COLOR, hsbColor); + updateState(CHANNEL_COLOR, hsbColor); logger.debug("Updating Color-Temperature Status: {} K {}%", lastColorTempInKelvin, lastColorTempInPercent); - updateState(COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); - updateState(COLOR_TEMPERATURE, new PercentType(lastColorTempInPercent)); + updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); + updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(lastColorTempInPercent)); } public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { 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 index 8eee24ae708ba..f66d3afbd1116 100644 --- 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 @@ -81,5 +81,5 @@ 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 = Handler for thing could not be added to list because ipaddress == null -offline.communication-error.empty-response = Offline due to receiving an empty response +offline.configuration-error.ip-address.missing = IP address is missing +offline.communication-error.empty-response = Empty response received From d0b20800f2ac933c020103235ed392b5d001141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 26 Nov 2023 16:10:48 +0100 Subject: [PATCH 23/29] fix thread warning, lower log level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../binding/govee/internal/GoveeHandler.java | 13 ++++++++----- .../govee/internal/RefreshStatusReceiver.java | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) 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 index 32553231dc189..5da037518bef7 100644 --- 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 @@ -170,10 +170,12 @@ public void initialize() { * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) */ public static void stopRefreshStatusJob() { - if (refreshStatusJob != null) { - refreshStatusJob.cancel(true); + ScheduledFuture refreshStatusJobFuture = refreshStatusJob; + if (refreshStatusJobFuture != null) { + refreshStatusJobFuture.cancel(true); + refreshStatusJob = null; } - refreshStatusJob = null; + refreshJobRunning = false; } @@ -191,8 +193,9 @@ public static synchronized void startRefreshStatusJob() { public void dispose() { super.dispose(); - if (triggerStatusJob != null) { - triggerStatusJob.cancel(true); + ScheduledFuture triggerStatusJobFuture = triggerStatusJob; + if (triggerStatusJobFuture != null) { + triggerStatusJobFuture.cancel(true); triggerStatusJob = null; } if (!goveeConfiguration.hostname.isEmpty()) { diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java index 52f1c697bfbe0..baad94ef79309 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java @@ -94,7 +94,7 @@ public void run() { thingHandler.statusUpdate(ThingStatus.ONLINE); } } catch (IOException e) { - logger.error("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); + logger.warn("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); // 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 From 403983d661e370cab9da18c004b919e32ee4c4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 2 Dec 2023 17:51:39 +0100 Subject: [PATCH 24/29] further review changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- bundles/org.openhab.binding.govee/README.md | 2 +- .../govee/internal/GoveeDiscoveryService.java | 68 +++++++++---------- .../binding/govee/internal/GoveeHandler.java | 6 +- .../govee/internal/RefreshStatusReceiver.java | 6 +- .../main/resources/OH-INF/config/config.xml | 8 --- .../resources/OH-INF/thing/thing-types.xml | 4 ++ 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index ec0e847f4baea..fca210a9b2cbf 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -123,7 +123,7 @@ arp -a | grep "MAC_ADDRESS" | color-temperature | Dimmer | Color Temperature Percentage | RW | | | color-temperature-abs | Dimmer | Color Temperature Absolute | RW | in 2000-9000 Kelvin | -Note: you may have to add "%.0f K" as the state description when creating a color-temperature-abs item. +Note: you may want to set Unit metadata to "K" when creating a color-temperature-abs item. ## UI Example for one device 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 index aa10b80bb54de..ccfd41d04393f 100644 --- 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 @@ -182,17 +182,19 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS final Map properties = getDeviceProperties(response); final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); final String productName = product != null ? product.toString() : "???"; - final Object mac = properties.get(GoveeBindingConstants.MAC_ADDRESS); - final String macAddress = mac != null ? mac.toString() : "???"; + final Object macAddress = properties.get(GoveeBindingConstants.MAC_ADDRESS); - ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_")); + if (macAddress != null) { + ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, + macAddress.toString().replace(":", "_")); - DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid).withProperties(properties) - .withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) - .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) + " (" - + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties).withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) + .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) + + " (" + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); - thingDiscovered(discoveryResult.build()); + thingDiscovered(discoveryResult.build()); + } } while (!Thread.currentThread().isInterrupted()); // left by SocketTimeoutException } @@ -201,41 +203,39 @@ public Map getDeviceProperties(String response) { Gson gson = new Gson(); DiscoveryResponse message = gson.fromJson(response, DiscoveryResponse.class); - String ipAddress = ""; - String sku = ""; + String ipAddress; + String sku; String macAddress = ""; String productName = ""; String hwVersion = "???"; String swVersion = "???"; - if (message != null) { - ipAddress = message.msg().data().ip(); - sku = message.msg().data().sku(); - macAddress = message.msg().data().device(); - hwVersion = message.msg().data().wifiVersionHard(); - swVersion = message.msg().data().wifiVersionSoft(); + ipAddress = message.msg().data().ip(); + sku = message.msg().data().sku(); + macAddress = message.msg().data().device(); + hwVersion = message.msg().data().wifiVersionHard(); + swVersion = message.msg().data().wifiVersionSoft(); - if (ipAddress.isEmpty()) { - ipAddress = "???"; - logger.warn("Empty IP Address received during discovery - device may not work"); - } + if (ipAddress.isEmpty()) { + ipAddress = "???"; + logger.warn("Empty IP Address received during discovery - device may not work"); + } - productName = "???"; - if (!sku.isEmpty()) { - final String skuLabel = "discovery.govee-light." + sku; - if (bundle != null) { - productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); - } - } else { - sku = "???"; - productName = "???"; - logger.warn("Empty SKU (product name) received during discovery - device may not work"); + productName = "???"; + if (!sku.isEmpty()) { + final String skuLabel = "discovery.govee-light." + sku; + if (bundle != null) { + productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); } + } else { + sku = "???"; + productName = "???"; + logger.warn("Empty SKU (product name) received during discovery - device may not work"); + } - if (macAddress.isEmpty()) { - macAddress = "???"; - logger.warn("Empty Mac Address received during discovery - device may not work"); - } + if (macAddress.isEmpty()) { + macAddress = "???"; + logger.warn("Empty Mac Address received during discovery - device may not work"); } Map properties = new HashMap<>(6); 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 index 5da037518bef7..dee8f9a9e4451 100644 --- 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 @@ -91,7 +91,7 @@ public class GoveeHandler extends BaseThingHandler { private static final Gson GSON = new Gson(); // Holds a list of all thing handlers to send them thing updates via the receiver-Thread - public static final Map THING_HANDLERS = new HashMap<>(); + private static final Map THING_HANDLERS = new HashMap<>(); private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class); private static final int SENDTODEVICE_PORT = 4003; @@ -387,4 +387,8 @@ public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nu public void statusUpdate(ThingStatus status) { updateStatus(status); } + + public static synchronized Map getThingHandlers() { + return THING_HANDLERS; + } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java index baad94ef79309..ea030212115c2 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java @@ -50,9 +50,9 @@ public void run() { } GoveeHandler.refreshJobRunning = true; - logger.trace("REFRESH: running refresh cycle for {} devices", GoveeHandler.THING_HANDLERS.size()); + logger.trace("REFRESH: running refresh cycle for {} devices", GoveeHandler.getThingHandlers().size()); - if (GoveeHandler.THING_HANDLERS.isEmpty()) { + if (GoveeHandler.getThingHandlers().isEmpty()) { return; } @@ -70,7 +70,7 @@ public void run() { logger.trace("Response from {} = {}", deviceIPAddress, response); logger.trace("received = {} from {}", response, deviceIPAddress); - thingHandler = GoveeHandler.THING_HANDLERS.get(deviceIPAddress); + thingHandler = GoveeHandler.getThingHandlers().get(deviceIPAddress); if (thingHandler == null) { logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress); return; 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 index 8f832f4675809..bb2a45e6909b8 100644 --- 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 @@ -18,14 +18,6 @@ The product number of the device - - - The Hardware Version of the device - - - - The Software Version of the device - Description of the device 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 index 002cb291f70c4..8f03d7e8bbd6c 100644 --- 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 @@ -13,6 +13,10 @@ + + + + From 039d6bc9337cc09b3b3c124255555de394929c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Tue, 12 Dec 2023 17:49:52 +0100 Subject: [PATCH 25/29] param/props change, status update noise reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../govee/internal/GoveeDiscoveryService.java | 107 ++++++++++-------- .../binding/govee/internal/GoveeHandler.java | 64 ++++++----- .../main/resources/OH-INF/config/config.xml | 8 -- .../resources/OH-INF/i18n/govee.properties | 5 - .../resources/OH-INF/thing/thing-types.xml | 4 - 5 files changed, 95 insertions(+), 93 deletions(-) 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 index ccfd41d04393f..783f6ec5294b7 100644 --- 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 @@ -47,6 +47,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * Discovers Govee Devices @@ -180,20 +181,22 @@ private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastS logger.trace("Govee Device Response: {}", response); final Map properties = getDeviceProperties(response); - final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); - final String productName = product != null ? product.toString() : "???"; - final Object macAddress = properties.get(GoveeBindingConstants.MAC_ADDRESS); + if (!properties.isEmpty()) { + final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); + final String productName = product != null ? product.toString() : "???"; + final Object macAddress = properties.get(GoveeBindingConstants.MAC_ADDRESS); - if (macAddress != null) { - ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, - macAddress.toString().replace(":", "_")); + if (macAddress != null) { + ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, + macAddress.toString().replace(":", "_")); - DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) - .withProperties(properties).withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) - .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) - + " (" + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) + .withProperties(properties).withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) + .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) + + " (" + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); - thingDiscovered(discoveryResult.build()); + thingDiscovered(discoveryResult.build()); + } } } while (!Thread.currentThread().isInterrupted()); // left by SocketTimeoutException } @@ -202,49 +205,53 @@ public Map getDeviceProperties(String response) { Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class); Gson gson = new Gson(); - DiscoveryResponse message = gson.fromJson(response, DiscoveryResponse.class); - String ipAddress; - String sku; - String macAddress = ""; - String productName = ""; - String hwVersion = "???"; - String swVersion = "???"; - - ipAddress = message.msg().data().ip(); - sku = message.msg().data().sku(); - macAddress = message.msg().data().device(); - hwVersion = message.msg().data().wifiVersionHard(); - swVersion = message.msg().data().wifiVersionSoft(); - - if (ipAddress.isEmpty()) { - ipAddress = "???"; - logger.warn("Empty IP Address received during discovery - device may not work"); - } - - productName = "???"; - if (!sku.isEmpty()) { - final String skuLabel = "discovery.govee-light." + sku; - if (bundle != null) { - productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); + Map properties = new HashMap<>(6); + try { + DiscoveryResponse message = gson.fromJson(response, DiscoveryResponse.class); + String ipAddress; + String sku; + String macAddress = ""; + String productName = ""; + String hwVersion = "???"; + String swVersion = "???"; + + ipAddress = message.msg().data().ip(); + sku = message.msg().data().sku(); + macAddress = message.msg().data().device(); + hwVersion = message.msg().data().wifiVersionHard(); + swVersion = message.msg().data().wifiVersionSoft(); + + if (ipAddress.isEmpty()) { + ipAddress = "???"; + logger.warn("Empty IP Address received during discovery - device may not work"); } - } else { - sku = "???"; + productName = "???"; - logger.warn("Empty SKU (product name) received during discovery - device may not work"); - } + if (!sku.isEmpty()) { + final String skuLabel = "discovery.govee-light." + sku; + if (bundle != null) { + productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); + } + } else { + sku = "???"; + productName = "???"; + logger.warn("Empty SKU (product name) received during discovery - device may not work"); + } - if (macAddress.isEmpty()) { - macAddress = "???"; - logger.warn("Empty Mac Address received during discovery - device may not work"); - } + if (macAddress.isEmpty()) { + macAddress = "???"; + logger.warn("Empty Mac Address received during discovery - device may not work"); + } - Map properties = new HashMap<>(6); - properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); - properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); - properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); - properties.put(GoveeBindingConstants.HW_VERSION, hwVersion); - properties.put(GoveeBindingConstants.SW_VERSION, swVersion); - properties.put(GoveeBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); + properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); + properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); + properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); + properties.put(GoveeBindingConstants.HW_VERSION, hwVersion); + properties.put(GoveeBindingConstants.SW_VERSION, swVersion); + properties.put(GoveeBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); + } catch (JsonSyntaxException e) { + logger.warn("Could not retrieve Device Properties as message {} is in the wrong format", response); + } return properties; } 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 index dee8f9a9e4451..b1d5eafbd4fd0 100644 --- 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 @@ -27,8 +27,6 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.measure.quantity.Temperature; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.govee.internal.model.Color; @@ -340,11 +338,11 @@ public void send(String message) throws IOException { * * @return the computed state */ - private HSBType getLastColorState() { - PercentType brightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(lastBrightness); - int rgb[] = { lastColor.r(), lastColor.g(), lastColor.b() }; + 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); - HSBType hsbState = new HSBType(hsb.getHue(), hsb.getSaturation(), brightness); + HSBType hsbState = new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness); return hsbState; } @@ -353,31 +351,45 @@ public void updateDeviceState(@Nullable StatusResponse message) { return; } - logger.debug("Update Device State ----------------------------------------------"); - lastOnOff = message.msg().data().onOff(); - logger.debug("lastOnOff = {}", lastOnOff); - lastBrightness = message.msg().data().brightness(); - logger.debug("lastbrightness = {}", lastBrightness); - lastColor = message.msg().data().color(); - logger.debug("lastColor = {}", lastColor); - lastColorTempInKelvin = message.msg().data().colorTemInKelvin(); - logger.debug("lastColorTempInKelvin = {}", lastColorTempInKelvin); - - lastColorTempInKelvin = (lastColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE) + logger.debug("Receiving Device State ----------------------------------------------"); + int newOnOff = message.msg().data().onOff(); + logger.debug("newOnOff = {}", newOnOff); + int newBrightness = message.msg().data().brightness(); + logger.debug("newBrightness = {}", newBrightness); + Color newColor = message.msg().data().color(); + logger.debug("newColor = {}", newColor); + int newColorTempInKelvin = message.msg().data().colorTemInKelvin(); + logger.debug("newColorTempInKelvin = {}", newColorTempInKelvin); + + newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE) ? COLOR_TEMPERATURE_MIN_VALUE.intValue() - : lastColorTempInKelvin; - int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE) + : newColorTempInKelvin; + int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE) / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue(); - logger.debug("Last RGB = {} {} {}, brightness {}", lastColor.r(), lastColor.g(), lastColor.b(), lastBrightness); + HSBType hsbColor = getColorState(newColor, newBrightness); + + final HSBType lastKnownColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() }); + logger.debug("HSB old: {} vs new: {}", lastKnownColor, hsbColor); + // avoid noise by only updating if the value has changed on the device + if (!hsbColor.equals(lastKnownColor)) { + logger.debug("UPDATING HSB old: {} != {}", lastKnownColor, hsbColor); + updateState(CHANNEL_COLOR, hsbColor); + } - HSBType hsbColor = getLastColorState(); - logger.debug("Send HSB = {} {} {}", hsbColor.getHue(), hsbColor.getSaturation(), hsbColor.getBrightness()); + // avoid noise by only updating if the value has changed on the device + logger.debug("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin, + newColorTempInPercent, newColorTempInKelvin); + if (newColorTempInKelvin != lastColorTempInKelvin) { + logger.debug("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)); + } - updateState(CHANNEL_COLOR, hsbColor); - logger.debug("Updating Color-Temperature Status: {} K {}%", lastColorTempInKelvin, lastColorTempInPercent); - updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType(lastColorTempInKelvin, Units.KELVIN)); - updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(lastColorTempInPercent)); + lastOnOff = newOnOff; + lastColor = newColor; + lastBrightness = newBrightness; } public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { 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 index bb2a45e6909b8..e153efa4d8067 100644 --- 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 @@ -14,14 +14,6 @@ MAC Address of the device - - - The product number of the device - - - - Description of the device - The amount of time that passes until the device is refreshed (in seconds) 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 index f66d3afbd1116..b600839cb40cf 100644 --- 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 @@ -13,11 +13,6 @@ thing-type.govee-light.description = Govee Light controllable via LAN API 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 -# channel types - -channel-type.color-temperature-abs.label = Color Temperature (Absolute) -channel-type.color-temperature-abs.description = Controls the color temperature of the light in Kelvin - # product names discovery.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights 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 index 8f03d7e8bbd6c..002cb291f70c4 100644 --- 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 @@ -13,10 +13,6 @@ - - - - From b97574b8f949bf6fa3e6f3d1d222377153c0f8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sat, 16 Dec 2023 16:28:03 +0100 Subject: [PATCH 26/29] new threadsafe implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../govee/internal/CommunicationManager.java | 255 ++++++++++++++++++ .../govee/internal/GoveeDiscoveryService.java | 223 +++++---------- .../binding/govee/internal/GoveeHandler.java | 184 ++++--------- .../govee/internal/RefreshStatusReceiver.java | 107 -------- .../govee/internal/GoveeDiscoveryTest.java | 14 +- 5 files changed, 390 insertions(+), 393 deletions(-) create mode 100644 bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java delete mode 100644 bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java 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..a5cf51a78eb40 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java @@ -0,0 +1,255 @@ +/** + * 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.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 it 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 +public class CommunicationManager { + private static final Gson GSON = new Gson(); + // Holds a list of all thing handlers to send them thing updates via the receiver-Thread + private static final Map THING_HANDLERS = new HashMap<>(); + @Nullable + private static 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); + } + + private CommunicationManager() { + } + + public static void registerHandler(GoveeHandler handler) { + synchronized (THING_HANDLERS) { + THING_HANDLERS.put(handler.getHostname(), handler); + if (receiverThread == null) { + receiverThread = new StatusReceiver(); + receiverThread.start(); + } + } + } + + public static void unregisterHandler(GoveeHandler handler) { + synchronized (THING_HANDLERS) { + THING_HANDLERS.remove(handler.getHostname()); + if (THING_HANDLERS.isEmpty()) { + StatusReceiver receiver = receiverThread; + if (receiver != null) { + receiver.stopReceiving(); + } + receiverThread = null; + } + } + } + + public static 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 static 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 static 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 (THING_HANDLERS) { + handler = THING_HANDLERS.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/GoveeDiscoveryService.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java index 783f6ec5294b7..843b47a96f703 100644 --- 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 @@ -14,27 +14,22 @@ package org.openhab.binding.govee.internal; import java.io.IOException; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; import java.net.NetworkInterface; import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Locale; -import java.util.Map; 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; @@ -46,9 +41,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - /** * Discovers Govee Devices * @@ -84,29 +76,21 @@ * @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 { - - public static boolean discoveryActive = false; - private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class); - private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250"; - private static final int DISCOVERY_PORT = 4001; - private static final int DISCOVERY_RESPONSE_PORT = 4002; - private static final int INTERFACE_TIMEOUT_SEC = 5; - private static final int MILLIS_PER_SEC = 1000; - - private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}"; - private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT); @Activate - public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider) { + public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, + @Reference LocaleProvider localeProvider) { super(SUPPORTED_THING_TYPES_UIDS, 0, false); this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; } // for test purposes only @@ -118,142 +102,77 @@ public GoveeDiscoveryService() { protected void startScan() { logger.debug("starting Scan"); - try { - discoveryActive = true; - - // check if the status receiver is currently running, stop it and wait for that. - // note that it restarts itself as soon as we are done. - while (GoveeHandler.isRefreshJobRunning()) { - GoveeHandler.stopRefreshStatusJob(); - Thread.sleep(1000); - logger.debug("Waiting for device status request finish its task to be able to start discovery"); + 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()); } - - InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS); - final InetSocketAddress socketAddress = new InetSocketAddress(broadcastAddress, DISCOVERY_RESPONSE_PORT); - - getLocalNetworkInterfaces().forEach(localNetworkInterface -> { - logger.debug("Discovering Govee Devices on {} ...", localNetworkInterface); - - try (MulticastSocket sendSocket = new MulticastSocket(socketAddress); - MulticastSocket receiveSocket = new MulticastSocket(DISCOVERY_RESPONSE_PORT)) { - sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); - sendSocket.setReuseAddress(true); - sendSocket.setBroadcast(true); - sendSocket.setTimeToLive(2); - sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, DISCOVERY_RESPONSE_PORT), - localNetworkInterface); - receiveSocket.setReuseAddress(true); - sendBroadcastToDiscoverThing(sendSocket, receiveSocket, broadcastAddress); - } catch (IOException e) { - logger.debug("Discovery with IO exception: {}", e.getMessage()); - } - }); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } catch (UnknownHostException e) { - logger.debug("Discovery with UnknownHostException exception: {}", e.getMessage()); - } finally { - discoveryActive = false; - } + logger.trace("After try"); + }); + logger.trace("Stopping Scan"); + // stopScan(); } - private void sendBroadcastToDiscoverThing(MulticastSocket sendSocket, MulticastSocket receiveSocket, - InetAddress broadcastAddress) throws IOException { - byte[] requestData = DISCOVER_REQUEST.getBytes(); - - DatagramPacket request = new DatagramPacket(requestData, requestData.length, broadcastAddress, DISCOVERY_PORT); - - try { - sendSocket.send(request); - } catch (SocketTimeoutException ste) { - // done with scanning + 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; } - receiveSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * MILLIS_PER_SEC); - receiveSocket.setReuseAddress(true); - do { - byte[] rxbuf = new byte[10240]; - DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length); - receiveSocket.receive(packet); - - String response = new String(packet.getData()).trim(); - logger.trace("Govee Device Response: {}", response); - - final Map properties = getDeviceProperties(response); - if (!properties.isEmpty()) { - final Object product = properties.get(GoveeBindingConstants.PRODUCT_NAME); - final String productName = product != null ? product.toString() : "???"; - final Object macAddress = properties.get(GoveeBindingConstants.MAC_ADDRESS); - - if (macAddress != null) { - ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, - macAddress.toString().replace(":", "_")); - - DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid) - .withProperties(properties).withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS) - .withLabel("Govee " + productName + " " + properties.get(GoveeBindingConstants.DEVICE_TYPE) - + " (" + properties.get(GoveeBindingConstants.IP_ADDRESS) + ")"); - - thingDiscovered(discoveryResult.build()); - } - } - } while (!Thread.currentThread().isInterrupted()); // left by SocketTimeoutException - } - - public Map getDeviceProperties(String response) { - Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class); - Gson gson = new Gson(); - - Map properties = new HashMap<>(6); - try { - DiscoveryResponse message = gson.fromJson(response, DiscoveryResponse.class); - String ipAddress; - String sku; - String macAddress = ""; - String productName = ""; - String hwVersion = "???"; - String swVersion = "???"; - - ipAddress = message.msg().data().ip(); - sku = message.msg().data().sku(); - macAddress = message.msg().data().device(); - hwVersion = message.msg().data().wifiVersionHard(); - swVersion = message.msg().data().wifiVersionSoft(); - - if (ipAddress.isEmpty()) { - ipAddress = "???"; - logger.warn("Empty IP Address received during discovery - device may not work"); - } + final String ipAddress = data.ip(); + if (ipAddress.isEmpty()) { + logger.warn("Empty IP Address received during discovery - ignoring {}", response); + return null; + } - productName = "???"; - if (!sku.isEmpty()) { - final String skuLabel = "discovery.govee-light." + sku; - if (bundle != null) { - productName = i18nProvider.getText(bundle, skuLabel, sku, Locale.getDefault()); - } - } else { - sku = "???"; - productName = "???"; - logger.warn("Empty SKU (product name) received during discovery - device may not work"); - } + final String sku = data.sku(); + if (sku.isEmpty()) { + logger.warn("Empty SKU (product name) received during discovery - ignoring {}", response); + return null; + } - if (macAddress.isEmpty()) { - macAddress = "???"; - logger.warn("Empty Mac Address received during discovery - device may not work"); - } + 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); + } - properties.put(GoveeBindingConstants.IP_ADDRESS, ipAddress); - properties.put(GoveeBindingConstants.DEVICE_TYPE, sku); - properties.put(GoveeBindingConstants.MAC_ADDRESS, macAddress); - properties.put(GoveeBindingConstants.HW_VERSION, hwVersion); - properties.put(GoveeBindingConstants.SW_VERSION, swVersion); - properties.put(GoveeBindingConstants.PRODUCT_NAME, (productName != null) ? productName : sku); - } catch (JsonSyntaxException e) { - logger.warn("Could not retrieve Device Properties as message {} is in the wrong format", response); + 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 properties; + return builder.build(); } private List getLocalNetworkInterfaces() { @@ -274,8 +193,4 @@ private List getLocalNetworkInterfaces() { } return result; } - - public static boolean isDiscoveryActive() { - return discoveryActive; - } } 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 index b1d5eafbd4fd0..c05da47c53905 100644 --- 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 @@ -19,11 +19,6 @@ import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MIN_VALUE; import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -36,7 +31,6 @@ 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.common.ThreadPoolManager; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -54,6 +48,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; /** * The {@link GoveeHandler} is responsible for handling commands, which are @@ -72,7 +67,7 @@ * * * The other topic that needs to be managed is that device discovery responses are also sent to openHAB at the same port - * like status updates. Therefore, when scanning new devices that job that listens to status devices must + * 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): @@ -88,35 +83,17 @@ public class GoveeHandler extends BaseThingHandler { */ private static final Gson GSON = new Gson(); - // Holds a list of all thing handlers to send them thing updates via the receiver-Thread - private static final Map THING_HANDLERS = new HashMap<>(); - private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class); - private static final int SENDTODEVICE_PORT = 4003; - public static final int RECEIVEFROMDEVICE_PORT = 4002; - - // Semaphores to suppress further processing if already running - public static boolean refreshJobRunning = false; - private static boolean refreshRunning = false; - @Nullable - private static ScheduledFuture refreshStatusJob; // device response receiver job @Nullable private ScheduledFuture triggerStatusJob; // send device status update job private GoveeConfiguration goveeConfiguration = new GoveeConfiguration(); private int lastOnOff; private int lastBrightness; - private Color lastColor = new Color(0, 0, 0); + private HSBType lastColor = new HSBType(); private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue(); - /* - * Common Receiver job for the status answers of the devices - */ - public static boolean isRefreshJobRunning() { - return refreshJobRunning && THING_HANDLERS.isEmpty(); - } - /** * 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 @@ -139,23 +116,22 @@ public GoveeHandler(Thing thing) { super(thing); } + public String getHostname() { + return goveeConfiguration.hostname; + } + @Override public void initialize() { goveeConfiguration = getConfigAs(GoveeConfiguration.class); final String ipAddress = goveeConfiguration.hostname; - if (!ipAddress.isEmpty()) { - THING_HANDLERS.put(ipAddress, this); - } else { + if (ipAddress.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.configuration-error.ip-address.missing"); return; } - if (!THING_HANDLERS.isEmpty()) { - startRefreshStatusJob(); - } - updateStatus(ThingStatus.UNKNOWN); + CommunicationManager.registerHandler(this); if (triggerStatusJob == null) { logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); @@ -164,29 +140,6 @@ public void initialize() { } } - /** - * Stop the Refresh Status Job, so the same socket can be used for something else (like discovery) - */ - public static void stopRefreshStatusJob() { - ScheduledFuture refreshStatusJobFuture = refreshStatusJob; - if (refreshStatusJobFuture != null) { - refreshStatusJobFuture.cancel(true); - refreshStatusJob = null; - } - - refreshJobRunning = false; - } - - /** - * (re)start the refresh status job - */ - public static synchronized void startRefreshStatusJob() { - if (refreshStatusJob == null) { - refreshStatusJob = ThreadPoolManager.getScheduledPool("goveeThingHandler") - .scheduleWithFixedDelay(new RefreshStatusReceiver(), 100, 1000, TimeUnit.MILLISECONDS); - } - } - @Override public void dispose() { super.dispose(); @@ -196,13 +149,7 @@ public void dispose() { triggerStatusJobFuture.cancel(true); triggerStatusJob = null; } - if (!goveeConfiguration.hostname.isEmpty()) { - THING_HANDLERS.remove(goveeConfiguration.hostname); - } - - if (THING_HANDLERS.isEmpty()) { - stopRefreshStatusJob(); - } + CommunicationManager.unregisterHandler(this); } @Override @@ -227,8 +174,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_COLOR_TEMPERATURE: if (command instanceof PercentType percent) { - logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", - command.toString()); + 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(); @@ -238,7 +184,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { break; case CHANNEL_COLOR_TEMPERATURE_ABS: if (command instanceof QuantityType quantity) { - logger.debug("Color Temperature Absolute change with Percent Type {}", command.toString()); + logger.debug("Color Temperature Absolute change with Percent Type {}", command); lastColorTempInKelvin = quantity.intValue(); logger.debug("COLOR_TEMPERATURE_ABS: lastColorTempInKelvin {}", lastColorTempInKelvin); int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin @@ -265,45 +211,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { * */ private void triggerDeviceStatusRefresh() throws IOException { - if (refreshRunning) { - return; - } - if (GoveeDiscoveryService.isDiscoveryActive()) { - logger.debug("Not triggering refresh as Scan is currently active"); - return; - } - refreshRunning = true; - logger.debug("trigger Refresh Status of device {}", thing.getLabel()); - - try { - GenericGoveeRequest lightQuery = new GenericGoveeRequest( - new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); - send(GSON.toJson(lightQuery)); - } finally { - refreshRunning = false; - } + GenericGoveeRequest lightQuery = new GenericGoveeRequest( + new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData())); + CommunicationManager.sendRequest(this, lightQuery); } public void sendColor(Color color) throws IOException { - lastColor = color; + lastColor = ColorUtil.rgbToHsb(new int[] { color.r(), color.g(), color.b() }); + GenericGoveeRequest lightColor = new GenericGoveeRequest( new GenericGoveeMsg("colorwc", new ColorData(color, 0))); - send(GSON.toJson(lightColor)); + CommunicationManager.sendRequest(this, lightColor); } public void sendBrightness(int brightness) throws IOException { lastBrightness = brightness; GenericGoveeRequest lightBrightness = new GenericGoveeRequest( new GenericGoveeMsg("brightness", new ValueIntData(brightness))); - send(GSON.toJson(lightBrightness)); + 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))); - send(GSON.toJson(switchLight)); + CommunicationManager.sendRequest(this, switchLight); } private void sendColorTemp(int colorTemp) throws IOException { @@ -311,20 +244,7 @@ private void sendColorTemp(int colorTemp) throws IOException { logger.debug("sendColorTemp {}", colorTemp); GenericGoveeRequest lightColor = new GenericGoveeRequest( new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp))); - send(GSON.toJson(lightColor)); - } - - public void send(String message) throws IOException { - DatagramSocket socket; - socket = new DatagramSocket(); - socket.setReuseAddress(true); - byte[] data = message.getBytes(); - - InetAddress address = InetAddress.getByName(goveeConfiguration.hostname); - logger.debug("Sending {} to {}", message, goveeConfiguration.hostname); - DatagramPacket packet = new DatagramPacket(data, data.length, address, SENDTODEVICE_PORT); - socket.send(packet); - socket.close(); + CommunicationManager.sendRequest(this, lightColor); } /** @@ -340,10 +260,27 @@ public void send(String message) throws IOException { */ 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() }; + int[] rgb = { color.r(), color.g(), color.b() }; HSBType hsb = ColorUtil.rgbToHsb(rgb); - HSBType hsbState = new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness); - return hsbState; + 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) { @@ -351,15 +288,15 @@ public void updateDeviceState(@Nullable StatusResponse message) { return; } - logger.debug("Receiving Device State ----------------------------------------------"); + logger.trace("Receiving Device State"); int newOnOff = message.msg().data().onOff(); - logger.debug("newOnOff = {}", newOnOff); + logger.trace("newOnOff = {}", newOnOff); int newBrightness = message.msg().data().brightness(); - logger.debug("newBrightness = {}", newBrightness); + logger.trace("newBrightness = {}", newBrightness); Color newColor = message.msg().data().color(); - logger.debug("newColor = {}", newColor); + logger.trace("newColor = {}", newColor); int newColorTempInKelvin = message.msg().data().colorTemInKelvin(); - logger.debug("newColorTempInKelvin = {}", newColorTempInKelvin); + logger.trace("newColorTempInKelvin = {}", newColorTempInKelvin); newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE) ? COLOR_TEMPERATURE_MIN_VALUE.intValue() @@ -367,40 +304,27 @@ public void updateDeviceState(@Nullable StatusResponse message) { int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE) / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue(); - HSBType hsbColor = getColorState(newColor, newBrightness); + HSBType adaptedColor = getColorState(newColor, newBrightness); - final HSBType lastKnownColor = ColorUtil.rgbToHsb(new int[] { lastColor.r(), lastColor.g(), lastColor.b() }); - logger.debug("HSB old: {} vs new: {}", lastKnownColor, hsbColor); + logger.trace("HSB old: {} vs adaptedColor: {}", lastColor, adaptedColor); // avoid noise by only updating if the value has changed on the device - if (!hsbColor.equals(lastKnownColor)) { - logger.debug("UPDATING HSB old: {} != {}", lastKnownColor, hsbColor); - updateState(CHANNEL_COLOR, hsbColor); + 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.debug("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin, + logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin, newColorTempInPercent, newColorTempInKelvin); if (newColorTempInKelvin != lastColorTempInKelvin) { - logger.debug("Color-Temperature Status: old: {} K {}% vs new: {} K", 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 = newColor; + lastColor = adaptedColor; lastBrightness = newBrightness; } - - public void statusUpdate(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { - updateStatus(status, statusDetail, description); - } - - public void statusUpdate(ThingStatus status) { - updateStatus(status); - } - - public static synchronized Map getThingHandlers() { - return THING_HANDLERS; - } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java deleted file mode 100644 index ea030212115c2..0000000000000 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/RefreshStatusReceiver.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 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.MulticastSocket; -import java.util.Arrays; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.govee.internal.model.StatusResponse; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * The {@link RefreshStatusReceiver} is a thread that handles the answers of all devices - * Therefore it needs to apply the information it 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 - */ -@NonNullByDefault -public class RefreshStatusReceiver implements Runnable { - final Logger logger = LoggerFactory.getLogger(RefreshStatusReceiver.class); - private static final Gson GSON = new Gson(); - - public RefreshStatusReceiver() { - } - - @Override - public void run() { - if (GoveeDiscoveryService.isDiscoveryActive()) { - logger.debug("Not running refresh as Scan is currently active"); - } - - GoveeHandler.refreshJobRunning = true; - logger.trace("REFRESH: running refresh cycle for {} devices", GoveeHandler.getThingHandlers().size()); - - if (GoveeHandler.getThingHandlers().isEmpty()) { - return; - } - - GoveeHandler thingHandler; - - try (MulticastSocket socket = new MulticastSocket(GoveeHandler.RECEIVEFROMDEVICE_PORT)) { - byte[] buffer = new byte[10240]; - DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket.setReuseAddress(true); - logger.debug("waiting for Status"); - socket.receive(packet); - - String response = new String(packet.getData()).trim(); - String deviceIPAddress = packet.getAddress().toString().replace("/", ""); - logger.trace("Response from {} = {}", deviceIPAddress, response); - logger.trace("received = {} from {}", response, deviceIPAddress); - - thingHandler = GoveeHandler.getThingHandlers().get(deviceIPAddress); - if (thingHandler == null) { - logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress); - return; - } - - logger.debug("updating status for thing {} ", thingHandler.getThing().getLabel()); - - if (!response.isEmpty()) { - try { - StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class); - thingHandler.updateDeviceState(statusMessage); - } catch (JsonSyntaxException jse) { - thingHandler.statusUpdate(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - jse.getMessage()); - } - } else { - thingHandler.statusUpdate(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.communication-error.empty-response"); - } - if (!thingHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) { - thingHandler.statusUpdate(ThingStatus.ONLINE); - } - } catch (IOException e) { - logger.warn("exception when receiving status packet {}", Arrays.toString(e.getStackTrace())); - // 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 { - GoveeHandler.refreshJobRunning = false; - } - } -} 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 index 2cee266ba10f2..05dd07b57ce88 100644 --- 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 @@ -16,9 +16,15 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; 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 @@ -46,8 +52,12 @@ public class GoveeDiscoveryTest { @Test public void testProcessScanMessage() { GoveeDiscoveryService service = new GoveeDiscoveryService(); - Map deviceProperties = service.getDeviceProperties(response); - assertNotNull(deviceProperties); + 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"); From 70a5eaf833400671a8dfa1615698c7c5f9f05df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20H=C3=B6hn?= Date: Sun, 17 Dec 2023 12:35:12 +0100 Subject: [PATCH 27/29] adapt text review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stefan Höhn --- .../binding/govee/internal/CommunicationManager.java | 6 +++--- .../binding/govee/internal/GoveeDiscoveryService.java | 10 ++++------ .../openhab/binding/govee/internal/GoveeHandler.java | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) 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 index a5cf51a78eb40..3cb82737249bb 100644 --- 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 @@ -34,10 +34,10 @@ 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 it to the right thing. + * 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 + * 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 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 index 843b47a96f703..a52439a7f9011 100644 --- 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 @@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory; /** - * Discovers Govee Devices + * Discovers Govee devices * * Scan approach: * 1. Determines all local network interfaces @@ -103,7 +103,7 @@ protected void startScan() { logger.debug("starting Scan"); getLocalNetworkInterfaces().forEach(localNetworkInterface -> { - logger.debug("Discovering Govee Devices on {} ...", localNetworkInterface); + logger.debug("Discovering Govee devices on {} ...", localNetworkInterface); try { CommunicationManager.runDiscoveryForInterface(localNetworkInterface, response -> { DiscoveryResult result = responseToResult(response); @@ -117,21 +117,19 @@ protected void startScan() { } logger.trace("After try"); }); - logger.trace("Stopping Scan"); - // stopScan(); } 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); + 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); + logger.warn("Empty IP address received during discovery - ignoring {}", response); return null; } 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 index c05da47c53905..051442e8d5bd9 100644 --- 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 @@ -79,7 +79,7 @@ public class GoveeHandler extends BaseThingHandler { /* - * Messages to be sent to the Govee Devices + * Messages to be sent to the Govee devices */ private static final Gson GSON = new Gson(); From 8e04f55c6187f6de3f6d8c7f8f042f8388cdfdb4 Mon Sep 17 00:00:00 2001 From: Kai Kreuzer Date: Sun, 17 Dec 2023 12:53:29 +0100 Subject: [PATCH 28/29] Change CommunicationManager to a service Signed-off-by: Kai Kreuzer --- .../govee/internal/CommunicationManager.java | 39 ++++++++++--------- .../govee/internal/GoveeDiscoveryService.java | 12 ++++-- .../binding/govee/internal/GoveeHandler.java | 25 ++++++------ .../govee/internal/GoveeHandlerFactory.java | 13 ++++++- .../govee/internal/GoveeDiscoveryTest.java | 5 +-- 5 files changed, 53 insertions(+), 41 deletions(-) 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 index 3cb82737249bb..fefdc454b4de0 100644 --- 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 @@ -27,6 +27,7 @@ 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.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,12 +44,13 @@ * @author Danny Baumann - Thread-Safe design refactoring */ @NonNullByDefault +@Component(service = CommunicationManager.class) public class CommunicationManager { - private static final Gson GSON = new Gson(); + private final Gson gson = new Gson(); // Holds a list of all thing handlers to send them thing updates via the receiver-Thread - private static final Map THING_HANDLERS = new HashMap<>(); + private final Map thingHandlers = new HashMap<>(); @Nullable - private static StatusReceiver receiverThread; + private StatusReceiver receiverThread; private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250"; private static final int DISCOVERY_PORT = 4001; @@ -63,12 +65,12 @@ public interface DiscoveryResultReceiver { void onResultReceived(DiscoveryResponse result); } - private CommunicationManager() { + CommunicationManager() { } - public static void registerHandler(GoveeHandler handler) { - synchronized (THING_HANDLERS) { - THING_HANDLERS.put(handler.getHostname(), handler); + public void registerHandler(GoveeHandler handler) { + synchronized (thingHandlers) { + thingHandlers.put(handler.getHostname(), handler); if (receiverThread == null) { receiverThread = new StatusReceiver(); receiverThread.start(); @@ -76,10 +78,10 @@ public static void registerHandler(GoveeHandler handler) { } } - public static void unregisterHandler(GoveeHandler handler) { - synchronized (THING_HANDLERS) { - THING_HANDLERS.remove(handler.getHostname()); - if (THING_HANDLERS.isEmpty()) { + public void unregisterHandler(GoveeHandler handler) { + synchronized (thingHandlers) { + thingHandlers.remove(handler.getHostname()); + if (thingHandlers.isEmpty()) { StatusReceiver receiver = receiverThread; if (receiver != null) { receiver.stopReceiving(); @@ -89,11 +91,11 @@ public static void unregisterHandler(GoveeHandler handler) { } } - public static void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException { + 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 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); @@ -102,8 +104,7 @@ public static void sendRequest(GoveeHandler handler, GenericGoveeRequest request socket.close(); } - public static void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultReceiver receiver) - throws IOException { + public void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultReceiver receiver) throws IOException { synchronized (receiver) { StatusReceiver localReceiver = null; StatusReceiver activeReceiver = null; @@ -158,7 +159,7 @@ public static void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResu } } - private static class StatusReceiver extends Thread { + private class StatusReceiver extends Thread { private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class); private boolean stopped = false; private @Nullable DiscoveryResultReceiver discoveryResultReceiver; @@ -213,7 +214,7 @@ public void run() { // 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); + DiscoveryResponse result = gson.fromJson(response, DiscoveryResponse.class); if (result != null) { synchronized (discoveryReceiver) { discoveryReceiver.onResultReceived(result); @@ -225,8 +226,8 @@ public void run() { } } else { final @Nullable GoveeHandler handler; - synchronized (THING_HANDLERS) { - handler = THING_HANDLERS.get(deviceIPAddress); + synchronized (thingHandlers) { + handler = thingHandlers.get(deviceIPAddress); } if (handler == null) { logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress); 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 index a52439a7f9011..03aaee0e98a1c 100644 --- 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 @@ -83,19 +83,23 @@ 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) { + 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() { + public GoveeDiscoveryService(CommunicationManager communicationManager) { super(SUPPORTED_THING_TYPES_UIDS, 0, false); + this.communicationManager = communicationManager; } @Override @@ -105,7 +109,7 @@ protected void startScan() { getLocalNetworkInterfaces().forEach(localNetworkInterface -> { logger.debug("Discovering Govee devices on {} ...", localNetworkInterface); try { - CommunicationManager.runDiscoveryForInterface(localNetworkInterface, response -> { + communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> { DiscoveryResult result = responseToResult(response); if (result != null) { thingDiscovered(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 index 051442e8d5bd9..1694541cad2d1 100644 --- 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 @@ -12,11 +12,7 @@ */ package org.openhab.binding.govee.internal; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MAX_VALUE; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.COLOR_TEMPERATURE_MIN_VALUE; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.*; import java.io.IOException; import java.util.concurrent.ScheduledFuture; @@ -89,6 +85,8 @@ public class GoveeHandler extends BaseThingHandler { 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(); @@ -112,8 +110,9 @@ public class GoveeHandler extends BaseThingHandler { } }; - public GoveeHandler(Thing thing) { + public GoveeHandler(Thing thing, CommunicationManager communicationManager) { super(thing); + this.communicationManager = communicationManager; } public String getHostname() { @@ -131,7 +130,7 @@ public void initialize() { return; } updateStatus(ThingStatus.UNKNOWN); - CommunicationManager.registerHandler(this); + communicationManager.registerHandler(this); if (triggerStatusJob == null) { logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel()); @@ -149,7 +148,7 @@ public void dispose() { triggerStatusJobFuture.cancel(true); triggerStatusJob = null; } - CommunicationManager.unregisterHandler(this); + communicationManager.unregisterHandler(this); } @Override @@ -214,7 +213,7 @@ 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); + communicationManager.sendRequest(this, lightQuery); } public void sendColor(Color color) throws IOException { @@ -222,21 +221,21 @@ public void sendColor(Color color) throws IOException { GenericGoveeRequest lightColor = new GenericGoveeRequest( new GenericGoveeMsg("colorwc", new ColorData(color, 0))); - CommunicationManager.sendRequest(this, lightColor); + 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); + 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); + communicationManager.sendRequest(this, switchLight); } private void sendColorTemp(int colorTemp) throws IOException { @@ -244,7 +243,7 @@ private void sendColorTemp(int colorTemp) throws IOException { logger.debug("sendColorTemp {}", colorTemp); GenericGoveeRequest lightColor = new GenericGoveeRequest( new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp))); - CommunicationManager.sendRequest(this, lightColor); + communicationManager.sendRequest(this, lightColor); } /** 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 index e8ad92330b3aa..120610fc95b58 100644 --- 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 @@ -12,7 +12,7 @@ */ package org.openhab.binding.govee.internal; -import static org.openhab.binding.govee.internal.GoveeBindingConstants.*; +import static org.openhab.binding.govee.internal.GoveeBindingConstants.THING_TYPE_LIGHT; import java.util.Set; @@ -23,7 +23,9 @@ 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 @@ -36,6 +38,13 @@ 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); @@ -46,7 +55,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_LIGHT.equals(thingTypeUID)) { - return new GoveeHandler(thing); + return new GoveeHandler(thing, communicationManager); } return null; 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 index 05dd07b57ce88..01c73d6e3c710 100644 --- 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 @@ -12,8 +12,7 @@ */ package org.openhab.binding.govee.internal; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; import java.util.Map; import java.util.Objects; @@ -51,7 +50,7 @@ public class GoveeDiscoveryTest { @Test public void testProcessScanMessage() { - GoveeDiscoveryService service = new GoveeDiscoveryService(); + GoveeDiscoveryService service = new GoveeDiscoveryService(new CommunicationManager()); DiscoveryResponse resp = new Gson().fromJson(response, DiscoveryResponse.class); Objects.requireNonNull(resp); @Nullable From 96dc9e9512bf4503924a002ce32ea463184accdc Mon Sep 17 00:00:00 2001 From: Kai Kreuzer Date: Sun, 17 Dec 2023 13:17:26 +0100 Subject: [PATCH 29/29] Make constructor public Signed-off-by: Kai Kreuzer --- .../openhab/binding/govee/internal/CommunicationManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index fefdc454b4de0..c3931dcafa94c 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -65,7 +66,8 @@ public interface DiscoveryResultReceiver { void onResultReceived(DiscoveryResponse result); } - CommunicationManager() { + @Activate + public CommunicationManager() { } public void registerHandler(GoveeHandler handler) {