diff --git a/CODEOWNERS b/CODEOWNERS
index 26ebb2c02cf2c..b3c666c0941c1 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -122,6 +122,7 @@
/bundles/org.openhab.binding.generacmobilelink/ @digitaldan
/bundles/org.openhab.binding.globalcache/ @mhilbush
/bundles/org.openhab.binding.goecharger/ @SamuelBrucksch
+/bundles/org.openhab.binding.govee/ @stefan-hoehn
/bundles/org.openhab.binding.gpio/ @nils-bauer
/bundles/org.openhab.binding.gpstracker/ @gbicskei
/bundles/org.openhab.binding.gree/ @markus7017
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 95eba8ac828c2..402dbf1be82c5 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -601,6 +601,11 @@
org.openhab.binding.goecharger${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.govee
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.gpio
diff --git a/bundles/org.openhab.binding.govee/NOTICE b/bundles/org.openhab.binding.govee/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md
new file mode 100644
index 0000000000000..fca210a9b2cbf
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/README.md
@@ -0,0 +1,170 @@
+# Govee Lan-API Binding
+
+![govee](doc/govee-lights.png)
+
+This binding integrates Light devices from [Govee](https://www.govee.com/).
+Even though these devices are widely used, they are usually only accessable via the Cloud.
+Another option is using Bluetooth which, due to its limitation only allows to control devices within a small range.
+The Bluetooth approach is supported by the openHAB Govee Binding while this binding covers the LAN interface.
+
+Fortunately, there is a [LAN API](https://app-h5.govee.com/user-manual/wlan-guide) that allows to control the devices within your own network without accessing the Cloud.
+Note, though, that is somehow limited to a number of devices listed in the aforementioned manual.
+The binding is aware of all the devices that are listed in that document and even provides a product description during discovery.
+
+Note: By intent the Cloud API has not been implemented (so far) as it makes controlling Govee devices dependent by Govee service itself.
+
+## Supported Things
+
+The things that are supported are all lights.
+While Govee provides probably more than a hundred different lights, only the following are supported officially by the LAN API, even though others might works as well.
+
+Here is a list of the supported devices (the ones marked with * have been tested by the author)
+
+- H619Z RGBIC Pro LED Strip Lights
+- H6046 RGBIC TV Light Bars
+- H6047 RGBIC Gaming Light Bars with Smart Controller
+- H6061 Glide Hexa LED Panels (*)
+- H6062 Glide Wall Light
+- H6065 Glide RGBIC Y Lights
+- H6066 Glide Hexa Pro LED Panel
+- H6067 Glide Triangle Light Panels (*)
+- H6072 RGBICWW Corner Floor Lamp
+- H6076 RGBICW Smart Corner Floor Lamp (*)
+- H6073 LED Floor Lamp
+- H6078 Cylinder Floor Lamp
+- H6087 RGBIC Smart Wall Sconces
+- H6173 RGBIC Outdoor Strip Lights
+- H619A RGBIC Strip Lights With Protective Coating 5M
+- H619B RGBIC LED Strip Lights With Protective Coating
+- H619C LED Strip Lights With Protective Coating
+- H619D RGBIC PRO LED Strip Lights
+- H619E RGBIC LED Strip Lights With Protective Coating
+- H61A0 RGBIC Neon Rope Light 1M
+- H61A1 RGBIC Neon Rope Light 2M
+- H61A2 RGBIC Neon Rope Light 5M
+- H61A3 RGBIC Neon Rope Light
+- H61A5 Neon LED Strip Light 10
+- H61A8Neon Neon Rope Light 10
+- H618A RGBIC Basic LED Strip Lights 5M
+- H618C RGBIC Basic LED Strip Lights 5M
+- H6117 Dream Color LED Strip Light 10M
+- H6159 RGB Light Strip (*)
+- H615E LED Strip Lights 30M
+- H6163 Dreamcolor LED Strip Light 5M
+- H610A Glide Lively Wall Lights
+- H610B Music Wall Lights
+- H6172 Outdoor LED Strip 10m
+- H61B2 RGBIC Neon TV Backlight
+- H61E1 LED Strip Light M1
+- H7012 Warm White Outdoor String Lights
+- H7013 Warm White Outdoor String Lights
+- H7021 RGBIC Warm White Smart Outdoor String
+- H7028 Lynx Dream LED-Bulb String
+- H7041 LED Outdoor Bulb String Lights
+- H7042 LED Outdoor Bulb String Lights
+- H705A Permanent Outdoor Lights 30M
+- H705B Permanent Outdoor Lights 15M
+- H7050 Outdoor Ground Lights 11M
+- H7051 Outdoor Ground Lights 15M
+- H7055 Pathway Light
+- H7060 LED Flood Lights (2-Pack)
+- H7061 LED Flood Lights (4-Pack)
+- H7062 LED Flood Lights (6-Pack)
+- H7065 Outdoor Spot Lights
+- H70C1 Govee Christmas String Lights 10m (*)
+- H70C2 Govee Christmas String Lights 20m (*)
+- H6051 Aura - Smart Table Lamp
+- H6056 H6056 Flow Plus
+- H6059 RGBWW Night Light for Kids
+- H618F RGBIC LED Strip Lights
+- H618E LED Strip Lights 22m
+- H6168 TV LED Backlight
+
+## Discovery
+
+Discovery is done by scanning the devices in the Thing section.
+
+The devices _do not_ support the LAN API support out-of-the-box.
+To be able to use the device with the LAN API, the following needs to be done (also see the "Preparations for LAN API Control" section in the [Goveee LAN API Manual](https://app-h5.govee.com/user-manual/wlan-guide)):
+
+- Start the Govee APP and add / discover the device (via Bluetooth) as described by the vendor manual
+ Go to the settings page of the device
+ ![govee device settings](doc/device-settings.png)
+- Note that it may take several(!) minutes until this setting comes up.
+- Switch on the LAN Control setting.
+- Now the device can be used with openHAB.
+- The easiest way is then to scan the devices via the SCAN button in the thing section of that binding
+
+## Thing Configuration
+
+Even though binding configuration is supported via a thing file it should be noted that the IP address is required and there is no easy way to find out the IP address of the device.
+One possibility is to look for the MAC address in the Govee app and then looking the IP address up via:
+
+```shell
+arp -a | grep "MAC_ADDRESS"
+```
+
+### `govee-light` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|---------------------------------------|---------|----------|----------|
+| hostname | text | Hostname or IP address of the device | N/A | yes | no |
+| macAddress | text | MAC address of the device | N/A | yes | no |
+| deviceType | text | The product number of the device | N/A | yes | no |
+| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
+
+## Channels
+
+| Channel | Type | Description | Read/Write | Description |
+|-----------------------|--------|---------------------------------|------------|----------------------|
+| color | Switch | On / Off | RW | Power On / OFF |
+| | Color | HSB (Hue Saturation Brightness) | RW | |
+| | Dimmer | Brightness Percentage | RW | |
+| color-temperature | Dimmer | Color Temperature Percentage | RW | |
+| color-temperature-abs | Dimmer | Color Temperature Absolute | RW | in 2000-9000 Kelvin |
+
+Note: you may want to set Unit metadata to "K" when creating a color-temperature-abs item.
+
+## UI Example for one device
+
+![ui-example.png](doc/ui-example.png)
+
+Thing channel setup:
+
+![channel-setup1.png](doc/channel-setup1.png)
+![channel-setup2.png](doc/channel-setup2.png)
+![channel-setup3.png](doc/channel-setup3.png)
+
+```java
+UID: govee:govee-light:33_5F_60_74_F4_08_77_21
+label: Govee H6159 RGB Light Strip H6159 (192.168.178.173)
+thingTypeUID: govee:govee-light
+configuration:
+ deviceType: H6159
+ wifiSoftwareVersion: 1.02.11
+ hostname: 192.168.162.233
+ macAddress: 33:5F:60:74:F4:08:66:21
+ wifiHardwareVersion: 1.00.10
+ refreshInterval: 5
+ productName: H6159 RGB Light Strip
+channels:
+ - id: color
+ channelTypeUID: system:color
+ label: Color
+ description: Controls the color of the light
+ configuration: {}
+ - id: color-temperature
+ channelTypeUID: system:color-temperature
+ label: Color Temperature
+ description: Controls the color temperature of the light from 0 (cold) to 100 (warm)
+ configuration: {}
+ - id: color-temperature-abs
+ channelTypeUID: govee:color-temperature-abs
+ label: Absolute Color Temperature
+ description: Controls the color temperature of the light in Kelvin
+ configuration: {}
+```
+
+## Additional Information
+
+Please provide any feedback regarding unlisted devices that even though not mentioned herein do work.
diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup1.png b/bundles/org.openhab.binding.govee/doc/channel-setup1.png
new file mode 100644
index 0000000000000..de86255dac23b
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup1.png differ
diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup2.png b/bundles/org.openhab.binding.govee/doc/channel-setup2.png
new file mode 100644
index 0000000000000..ab770d5502946
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup2.png differ
diff --git a/bundles/org.openhab.binding.govee/doc/channel-setup3.png b/bundles/org.openhab.binding.govee/doc/channel-setup3.png
new file mode 100644
index 0000000000000..52b4675793ae3
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/channel-setup3.png differ
diff --git a/bundles/org.openhab.binding.govee/doc/device-settings.png b/bundles/org.openhab.binding.govee/doc/device-settings.png
new file mode 100644
index 0000000000000..053096ce0abc5
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/device-settings.png differ
diff --git a/bundles/org.openhab.binding.govee/doc/govee-lights.png b/bundles/org.openhab.binding.govee/doc/govee-lights.png
new file mode 100644
index 0000000000000..1917086dd66b7
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/govee-lights.png differ
diff --git a/bundles/org.openhab.binding.govee/doc/ui-example.png b/bundles/org.openhab.binding.govee/doc/ui-example.png
new file mode 100644
index 0000000000000..fd23c6b0b6be4
Binary files /dev/null and b/bundles/org.openhab.binding.govee/doc/ui-example.png differ
diff --git a/bundles/org.openhab.binding.govee/pom.xml b/bundles/org.openhab.binding.govee/pom.xml
new file mode 100644
index 0000000000000..6a7245634acb3
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.1.0-SNAPSHOT
+
+
+ org.openhab.binding.govee
+
+ openHAB Add-ons :: Bundles :: Govee Binding
+
+
diff --git a/bundles/org.openhab.binding.govee/src/main/feature/feature.xml b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..8d0ae40c5d9fc
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.govee/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java
new file mode 100644
index 0000000000000..c3931dcafa94c
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java
@@ -0,0 +1,258 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.govee.internal.model.DiscoveryResponse;
+import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+
+/**
+ * The {@link CommunicationManager} is a thread that handles the answers of all devices.
+ * Therefore it needs to apply the information to the right thing.
+ *
+ * Discovery uses the same response code, so we must not refresh the status during discovery.
+ *
+ * @author Stefan Höhn - Initial contribution
+ * @author Danny Baumann - Thread-Safe design refactoring
+ */
+@NonNullByDefault
+@Component(service = CommunicationManager.class)
+public class CommunicationManager {
+ private final Gson gson = new Gson();
+ // Holds a list of all thing handlers to send them thing updates via the receiver-Thread
+ private final Map thingHandlers = new HashMap<>();
+ @Nullable
+ private StatusReceiver receiverThread;
+
+ private static final String DISCOVERY_MULTICAST_ADDRESS = "239.255.255.250";
+ private static final int DISCOVERY_PORT = 4001;
+ private static final int RESPONSE_PORT = 4002;
+ private static final int REQUEST_PORT = 4003;
+
+ private static final int INTERFACE_TIMEOUT_SEC = 5;
+
+ private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";
+
+ public interface DiscoveryResultReceiver {
+ void onResultReceived(DiscoveryResponse result);
+ }
+
+ @Activate
+ public CommunicationManager() {
+ }
+
+ public void registerHandler(GoveeHandler handler) {
+ synchronized (thingHandlers) {
+ thingHandlers.put(handler.getHostname(), handler);
+ if (receiverThread == null) {
+ receiverThread = new StatusReceiver();
+ receiverThread.start();
+ }
+ }
+ }
+
+ public void unregisterHandler(GoveeHandler handler) {
+ synchronized (thingHandlers) {
+ thingHandlers.remove(handler.getHostname());
+ if (thingHandlers.isEmpty()) {
+ StatusReceiver receiver = receiverThread;
+ if (receiver != null) {
+ receiver.stopReceiving();
+ }
+ receiverThread = null;
+ }
+ }
+ }
+
+ public void sendRequest(GoveeHandler handler, GenericGoveeRequest request) throws IOException {
+ final String hostname = handler.getHostname();
+ final DatagramSocket socket = new DatagramSocket();
+ socket.setReuseAddress(true);
+ final String message = gson.toJson(request);
+ final byte[] data = message.getBytes();
+ final InetAddress address = InetAddress.getByName(hostname);
+ DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
+ // logger.debug("Sending {} to {}", message, hostname);
+ socket.send(packet);
+ socket.close();
+ }
+
+ public void runDiscoveryForInterface(NetworkInterface intf, DiscoveryResultReceiver receiver) throws IOException {
+ synchronized (receiver) {
+ StatusReceiver localReceiver = null;
+ StatusReceiver activeReceiver = null;
+
+ try {
+ if (receiverThread == null) {
+ localReceiver = new StatusReceiver();
+ localReceiver.start();
+ activeReceiver = localReceiver;
+ } else {
+ activeReceiver = receiverThread;
+ }
+
+ if (activeReceiver != null) {
+ activeReceiver.setDiscoveryResultsReceiver(receiver);
+ }
+
+ final InetAddress broadcastAddress = InetAddress.getByName(DISCOVERY_MULTICAST_ADDRESS);
+ final InetSocketAddress socketAddress = new InetSocketAddress(broadcastAddress, RESPONSE_PORT);
+ final Instant discoveryStartTime = Instant.now();
+ final Instant discoveryEndTime = discoveryStartTime.plusSeconds(INTERFACE_TIMEOUT_SEC);
+
+ try (MulticastSocket sendSocket = new MulticastSocket(socketAddress)) {
+ sendSocket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000);
+ sendSocket.setReuseAddress(true);
+ sendSocket.setBroadcast(true);
+ sendSocket.setTimeToLive(2);
+ sendSocket.joinGroup(new InetSocketAddress(broadcastAddress, RESPONSE_PORT), intf);
+
+ byte[] requestData = DISCOVER_REQUEST.getBytes();
+
+ DatagramPacket request = new DatagramPacket(requestData, requestData.length, broadcastAddress,
+ DISCOVERY_PORT);
+ sendSocket.send(request);
+ }
+
+ do {
+ try {
+ receiver.wait(INTERFACE_TIMEOUT_SEC * 1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ } while (Instant.now().isBefore(discoveryEndTime));
+ } finally {
+ if (activeReceiver != null) {
+ activeReceiver.setDiscoveryResultsReceiver(null);
+ }
+ if (localReceiver != null) {
+ localReceiver.stopReceiving();
+ }
+ }
+ }
+ }
+
+ private class StatusReceiver extends Thread {
+ private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
+ private boolean stopped = false;
+ private @Nullable DiscoveryResultReceiver discoveryResultReceiver;
+
+ private @Nullable MulticastSocket socket;
+
+ StatusReceiver() {
+ super("GoveeStatusReceiver");
+ }
+
+ synchronized void setDiscoveryResultsReceiver(@Nullable DiscoveryResultReceiver receiver) {
+ discoveryResultReceiver = receiver;
+ }
+
+ void stopReceiving() {
+ stopped = true;
+ interrupt();
+ if (socket != null) {
+ socket.close();
+ }
+
+ try {
+ join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public void run() {
+ while (!stopped) {
+ try {
+ socket = new MulticastSocket(RESPONSE_PORT);
+ byte[] buffer = new byte[10240];
+ socket.setReuseAddress(true);
+ while (!stopped) {
+ DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
+ socket.receive(packet);
+ if (stopped) {
+ break;
+ }
+
+ String response = new String(packet.getData(), packet.getOffset(), packet.getLength());
+ String deviceIPAddress = packet.getAddress().toString().replace("/", "");
+ logger.trace("Response from {} = {}", deviceIPAddress, response);
+
+ final DiscoveryResultReceiver discoveryReceiver;
+ synchronized (this) {
+ discoveryReceiver = discoveryResultReceiver;
+ }
+ if (discoveryReceiver != null) {
+ // We're in discovery mode: try to parse result as discovery message and signal the receiver
+ // if parsing was successful
+ try {
+ DiscoveryResponse result = gson.fromJson(response, DiscoveryResponse.class);
+ if (result != null) {
+ synchronized (discoveryReceiver) {
+ discoveryReceiver.onResultReceived(result);
+ discoveryReceiver.notifyAll();
+ }
+ }
+ } catch (JsonParseException e) {
+ // this probably was a status message
+ }
+ } else {
+ final @Nullable GoveeHandler handler;
+ synchronized (thingHandlers) {
+ handler = thingHandlers.get(deviceIPAddress);
+ }
+ if (handler == null) {
+ logger.warn("thing Handler for {} couldn't be found.", deviceIPAddress);
+ } else {
+ logger.debug("processing status updates for thing {} ", handler.getThing().getLabel());
+ handler.handleIncomingStatus(response);
+ }
+ }
+ }
+ } catch (IOException e) {
+ logger.warn("exception when receiving status packet", e);
+ // as we haven't received a packet we also don't know where it should have come from
+ // hence, we don't know which thing put offline.
+ // a way to monitor this would be to keep track in a list, which device answers we expect
+ // and supervise an expected answer within a given time but that will make the whole
+ // mechanism much more complicated and may be added in the future
+ } finally {
+ if (socket != null) {
+ socket.close();
+ socket = null;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java
new file mode 100644
index 0000000000000..280a08adcce29
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link GoveeBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeBindingConstants {
+
+ // Thing properties
+ public static final String MAC_ADDRESS = "macAddress";
+ public static final String IP_ADDRESS = "hostname";
+ public static final String DEVICE_TYPE = "deviceType";
+ public static final String PRODUCT_NAME = "productName";
+ public static final String HW_VERSION = "wifiHardwareVersion";
+ public static final String SW_VERSION = "wifiSoftwareVersion";
+ private static final String BINDING_ID = "govee";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_LIGHT = new ThingTypeUID(BINDING_ID, "govee-light");
+
+ // List of all Channel ids
+ public static final String CHANNEL_COLOR = "color";
+ public static final String CHANNEL_COLOR_TEMPERATURE = "color-temperature";
+ public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "color-temperature-abs";
+
+ // Limit values of channels
+ public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0;
+ public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.0;
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java
new file mode 100644
index 0000000000000..319cb573e2539
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeConfiguration {
+
+ public String hostname = "";
+ public int refreshInterval = 5; // in seconds
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java
new file mode 100644
index 0000000000000..03aaee0e98a1c
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.govee.internal;
+
+import java.io.IOException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.govee.internal.model.DiscoveryData;
+import org.openhab.binding.govee.internal.model.DiscoveryResponse;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovers Govee devices
+ *
+ * Scan approach:
+ * 1. Determines all local network interfaces
+ * 2. Send a multicast message on each interface to the Govee multicast address 239.255.255.250 at port 4001
+ * 3. Retrieve the list of devices
+ *
+ * Based on the description at https://app-h5.govee.com/user-manual/wlan-guide
+ *
+ * A typical scan response looks as follows
+ *
+ *
+ *
+ * Note that it uses the same port for receiving data like when receiving devices status updates.
+ *
+ * @see GoveeHandler
+ *
+ * @author Stefan Höhn - Initial Contribution
+ * @author Danny Baumann - Thread-Safe design refactoring
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee")
+public class GoveeDiscoveryService extends AbstractDiscoveryService {
+ private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class);
+
+ private CommunicationManager communicationManager;
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(GoveeBindingConstants.THING_TYPE_LIGHT);
+
+ @Activate
+ public GoveeDiscoveryService(@Reference TranslationProvider i18nProvider, @Reference LocaleProvider localeProvider,
+ @Reference CommunicationManager communicationManager) {
+ super(SUPPORTED_THING_TYPES_UIDS, 0, false);
+ this.i18nProvider = i18nProvider;
+ this.localeProvider = localeProvider;
+ this.communicationManager = communicationManager;
+ }
+
+ // for test purposes only
+ public GoveeDiscoveryService(CommunicationManager communicationManager) {
+ super(SUPPORTED_THING_TYPES_UIDS, 0, false);
+ this.communicationManager = communicationManager;
+ }
+
+ @Override
+ protected void startScan() {
+ logger.debug("starting Scan");
+
+ getLocalNetworkInterfaces().forEach(localNetworkInterface -> {
+ logger.debug("Discovering Govee devices on {} ...", localNetworkInterface);
+ try {
+ communicationManager.runDiscoveryForInterface(localNetworkInterface, response -> {
+ DiscoveryResult result = responseToResult(response);
+ if (result != null) {
+ thingDiscovered(result);
+ }
+ });
+ logger.trace("After runDiscoveryForInterface");
+ } catch (IOException e) {
+ logger.debug("Discovery with IO exception: {}", e.getMessage());
+ }
+ logger.trace("After try");
+ });
+ }
+
+ public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
+ final DiscoveryData data = response.msg().data();
+ final String macAddress = data.device();
+ if (macAddress.isEmpty()) {
+ logger.warn("Empty Mac address received during discovery - ignoring {}", response);
+ return null;
+ }
+
+ final String ipAddress = data.ip();
+ if (ipAddress.isEmpty()) {
+ logger.warn("Empty IP address received during discovery - ignoring {}", response);
+ return null;
+ }
+
+ final String sku = data.sku();
+ if (sku.isEmpty()) {
+ logger.warn("Empty SKU (product name) received during discovery - ignoring {}", response);
+ return null;
+ }
+
+ final String productName;
+ if (i18nProvider != null) {
+ Bundle bundle = FrameworkUtil.getBundle(GoveeDiscoveryService.class);
+ productName = i18nProvider.getText(bundle, "discovery.govee-light." + sku, null,
+ localeProvider.getLocale());
+ } else {
+ productName = sku;
+ }
+ String nameForLabel = productName != null ? productName + " " + sku : sku;
+
+ ThingUID thingUid = new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, macAddress.replace(":", "_"));
+ DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUid)
+ .withRepresentationProperty(GoveeBindingConstants.MAC_ADDRESS)
+ .withProperty(GoveeBindingConstants.MAC_ADDRESS, macAddress)
+ .withProperty(GoveeBindingConstants.IP_ADDRESS, ipAddress)
+ .withProperty(GoveeBindingConstants.DEVICE_TYPE, sku)
+ .withLabel(String.format("Govee %s (%s)", nameForLabel, ipAddress));
+
+ if (productName != null) {
+ builder.withProperty(GoveeBindingConstants.PRODUCT_NAME, productName);
+ }
+
+ String hwVersion = data.wifiVersionHard();
+ if (hwVersion != null) {
+ builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
+ }
+ String swVersion = data.wifiVersionSoft();
+ if (swVersion != null) {
+ builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
+ }
+
+ return builder.build();
+ }
+
+ private List getLocalNetworkInterfaces() {
+ List result = new LinkedList<>();
+ try {
+ for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ try {
+ if (networkInterface.isUp() && !networkInterface.isLoopback()
+ && !networkInterface.isPointToPoint()) {
+ result.add(networkInterface);
+ }
+ } catch (SocketException exception) {
+ // ignore
+ }
+ }
+ } catch (SocketException exception) {
+ return List.of();
+ }
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java
new file mode 100644
index 0000000000000..1694541cad2d1
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java
@@ -0,0 +1,329 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.govee.internal.model.Color;
+import org.openhab.binding.govee.internal.model.ColorData;
+import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData;
+import org.openhab.binding.govee.internal.model.GenericGoveeMsg;
+import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
+import org.openhab.binding.govee.internal.model.StatusResponse;
+import org.openhab.binding.govee.internal.model.ValueIntData;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.util.ColorUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link GoveeHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * Any device has its own job that triggers a refresh of retrieving the external state from the device.
+ * However, there must be only one job that listens for all devices in a singleton thread because
+ * all devices send their udp packet response to the same port on openHAB. Based on the sender IP address
+ * of the device we can detect to which thing the status answer needs to be assigned to and updated.
+ *
+ *
+ *
The job per thing that triggers a new update is called triggerStatusJob. There are as many instances
+ * as things.
+ *
The job that receives the answers and applies that to the respective thing is called refreshStatusJob and
+ * there is only one for all instances. It may be stopped and restarted by the DiscoveryService (see below).
+ *
+ *
+ * The other topic that needs to be managed is that device discovery responses are also sent to openHAB at the same port
+ * as status updates. Therefore, when scanning new devices that job that listens to status devices must
+ * be stopped while scanning new devices. Otherwise, the status job will receive the scan discover UDB packages.
+ *
+ * Controlling the lights is done via the Govee LAN API (cloud is not supported):
+ * https://app-h5.govee.com/user-manual/wlan-guide
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeHandler extends BaseThingHandler {
+
+ /*
+ * Messages to be sent to the Govee devices
+ */
+ private static final Gson GSON = new Gson();
+
+ private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
+
+ @Nullable
+ private ScheduledFuture> triggerStatusJob; // send device status update job
+ private GoveeConfiguration goveeConfiguration = new GoveeConfiguration();
+
+ private CommunicationManager communicationManager;
+
+ private int lastOnOff;
+ private int lastBrightness;
+ private HSBType lastColor = new HSBType();
+ private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue();
+
+ /**
+ * This thing related job thingRefreshSender triggers an update to the Govee device.
+ * The device sends it back to the common port and the response is
+ * then received by the common #refreshStatusReceiver
+ */
+ private final Runnable thingRefreshSender = () -> {
+ try {
+ triggerDeviceStatusRefresh();
+ if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ + "\"]");
+ }
+ };
+
+ public GoveeHandler(Thing thing, CommunicationManager communicationManager) {
+ super(thing);
+ this.communicationManager = communicationManager;
+ }
+
+ public String getHostname() {
+ return goveeConfiguration.hostname;
+ }
+
+ @Override
+ public void initialize() {
+ goveeConfiguration = getConfigAs(GoveeConfiguration.class);
+
+ final String ipAddress = goveeConfiguration.hostname;
+ if (ipAddress.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.configuration-error.ip-address.missing");
+ return;
+ }
+ updateStatus(ThingStatus.UNKNOWN);
+ communicationManager.registerHandler(this);
+ if (triggerStatusJob == null) {
+ logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
+
+ triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100,
+ goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ ScheduledFuture> triggerStatusJobFuture = triggerStatusJob;
+ if (triggerStatusJobFuture != null) {
+ triggerStatusJobFuture.cancel(true);
+ triggerStatusJob = null;
+ }
+ communicationManager.unregisterHandler(this);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ try {
+ if (command instanceof RefreshType) {
+ // we are refreshing all channels at once, as we get all information at the same time
+ triggerDeviceStatusRefresh();
+ logger.debug("Triggering Refresh");
+ } else {
+ logger.debug("Channel ID {} type {}", channelUID.getId(), command.getClass());
+ switch (channelUID.getId()) {
+ case CHANNEL_COLOR:
+ if (command instanceof HSBType hsbCommand) {
+ int[] rgb = ColorUtil.hsbToRgb(hsbCommand);
+ sendColor(new Color(rgb[0], rgb[1], rgb[2]));
+ } else if (command instanceof PercentType percent) {
+ sendBrightness(percent.intValue());
+ } else if (command instanceof OnOffType onOffCommand) {
+ sendOnOff(onOffCommand);
+ }
+ break;
+ case CHANNEL_COLOR_TEMPERATURE:
+ if (command instanceof PercentType percent) {
+ logger.debug("COLOR_TEMPERATURE: Color Temperature change with Percent Type {}", command);
+ Double colorTemp = (COLOR_TEMPERATURE_MIN_VALUE + percent.intValue()
+ * (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) / 100.0);
+ lastColorTempInKelvin = colorTemp.intValue();
+ logger.debug("lastColorTempInKelvin {}", lastColorTempInKelvin);
+ sendColorTemp(lastColorTempInKelvin);
+ }
+ break;
+ case CHANNEL_COLOR_TEMPERATURE_ABS:
+ if (command instanceof QuantityType> quantity) {
+ logger.debug("Color Temperature Absolute change with Percent Type {}", command);
+ lastColorTempInKelvin = quantity.intValue();
+ logger.debug("COLOR_TEMPERATURE_ABS: lastColorTempInKelvin {}", lastColorTempInKelvin);
+ int lastColorTempInPercent = ((Double) ((lastColorTempInKelvin
+ - COLOR_TEMPERATURE_MIN_VALUE)
+ / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
+ logger.debug("computed lastColorTempInPercent {}", lastColorTempInPercent);
+ sendColorTemp(lastColorTempInKelvin);
+ }
+ break;
+ }
+ }
+ if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
+ + "\"]");
+ }
+ }
+
+ /**
+ * Initiate a refresh to our thing devicee
+ *
+ */
+ private void triggerDeviceStatusRefresh() throws IOException {
+ logger.debug("trigger Refresh Status of device {}", thing.getLabel());
+ GenericGoveeRequest lightQuery = new GenericGoveeRequest(
+ new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData()));
+ communicationManager.sendRequest(this, lightQuery);
+ }
+
+ public void sendColor(Color color) throws IOException {
+ lastColor = ColorUtil.rgbToHsb(new int[] { color.r(), color.g(), color.b() });
+
+ GenericGoveeRequest lightColor = new GenericGoveeRequest(
+ new GenericGoveeMsg("colorwc", new ColorData(color, 0)));
+ communicationManager.sendRequest(this, lightColor);
+ }
+
+ public void sendBrightness(int brightness) throws IOException {
+ lastBrightness = brightness;
+ GenericGoveeRequest lightBrightness = new GenericGoveeRequest(
+ new GenericGoveeMsg("brightness", new ValueIntData(brightness)));
+ communicationManager.sendRequest(this, lightBrightness);
+ }
+
+ private void sendOnOff(OnOffType switchValue) throws IOException {
+ lastOnOff = (switchValue == OnOffType.ON) ? 1 : 0;
+ GenericGoveeRequest switchLight = new GenericGoveeRequest(
+ new GenericGoveeMsg("turn", new ValueIntData(lastOnOff)));
+ communicationManager.sendRequest(this, switchLight);
+ }
+
+ private void sendColorTemp(int colorTemp) throws IOException {
+ lastColorTempInKelvin = colorTemp;
+ logger.debug("sendColorTemp {}", colorTemp);
+ GenericGoveeRequest lightColor = new GenericGoveeRequest(
+ new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 0, 0), colorTemp)));
+ communicationManager.sendRequest(this, lightColor);
+ }
+
+ /**
+ * Creates a Color state by using the last color information from lastColor
+ * The brightness is overwritten either by the provided lastBrightness
+ * or if lastOnOff = 0 (off) then the brightness is set 0
+ *
+ * @see #lastColor
+ * @see #lastBrightness
+ * @see #lastOnOff
+ *
+ * @return the computed state
+ */
+ private HSBType getColorState(Color color, int brightness) {
+ PercentType computedBrightness = lastOnOff == 0 ? new PercentType(0) : new PercentType(brightness);
+ int[] rgb = { color.r(), color.g(), color.b() };
+ HSBType hsb = ColorUtil.rgbToHsb(rgb);
+ return new HSBType(hsb.getHue(), hsb.getSaturation(), computedBrightness);
+ }
+
+ void handleIncomingStatus(String response) {
+ if (response.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.communication-error.empty-response");
+ return;
+ }
+
+ try {
+ StatusResponse statusMessage = GSON.fromJson(response, StatusResponse.class);
+ if (statusMessage != null) {
+ updateDeviceState(statusMessage);
+ }
+ updateStatus(ThingStatus.ONLINE);
+ } catch (JsonSyntaxException jse) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, jse.getMessage());
+ }
+ }
+
+ public void updateDeviceState(@Nullable StatusResponse message) {
+ if (message == null) {
+ return;
+ }
+
+ logger.trace("Receiving Device State");
+ int newOnOff = message.msg().data().onOff();
+ logger.trace("newOnOff = {}", newOnOff);
+ int newBrightness = message.msg().data().brightness();
+ logger.trace("newBrightness = {}", newBrightness);
+ Color newColor = message.msg().data().color();
+ logger.trace("newColor = {}", newColor);
+ int newColorTempInKelvin = message.msg().data().colorTemInKelvin();
+ logger.trace("newColorTempInKelvin = {}", newColorTempInKelvin);
+
+ newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE)
+ ? COLOR_TEMPERATURE_MIN_VALUE.intValue()
+ : newColorTempInKelvin;
+ int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE)
+ / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
+
+ HSBType adaptedColor = getColorState(newColor, newBrightness);
+
+ logger.trace("HSB old: {} vs adaptedColor: {}", lastColor, adaptedColor);
+ // avoid noise by only updating if the value has changed on the device
+ if (!adaptedColor.equals(lastColor)) {
+ logger.trace("UPDATING HSB old: {} != {}", lastColor, adaptedColor);
+ updateState(CHANNEL_COLOR, adaptedColor);
+ }
+
+ // avoid noise by only updating if the value has changed on the device
+ logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
+ newColorTempInPercent, newColorTempInKelvin);
+ if (newColorTempInKelvin != lastColorTempInKelvin) {
+ logger.trace("Color-Temperature Status: old: {} K {}% vs new: {} K", lastColorTempInKelvin,
+ newColorTempInPercent, newColorTempInKelvin);
+ updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType<>(lastColorTempInKelvin, Units.KELVIN));
+ updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(newColorTempInPercent));
+ }
+
+ lastOnOff = newOnOff;
+ lastColor = adaptedColor;
+ lastBrightness = newBrightness;
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java
new file mode 100644
index 0000000000000..120610fc95b58
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import static org.openhab.binding.govee.internal.GoveeBindingConstants.THING_TYPE_LIGHT;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link GoveeHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.govee", service = ThingHandlerFactory.class)
+public class GoveeHandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT);
+
+ private CommunicationManager communicationManager;
+
+ @Activate
+ public GoveeHandlerFactory(@Reference CommunicationManager communicationManager) {
+ this.communicationManager = communicationManager;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_LIGHT.equals(thingTypeUID)) {
+ return new GoveeHandler(thing, communicationManager);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java
new file mode 100644
index 0000000000000..88c8dd4bbe659
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/Color.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+/**
+ *
+ * @param r red
+ * @param g green
+ * @param b blue
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+public record Color(int r, int g, int b) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java
new file mode 100644
index 0000000000000..c46fded8ab77a
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ColorData.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Color Data
+ *
+ * @param color
+ * @param colorTemInKelvin
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record ColorData(Color color, int colorTemInKelvin) implements GenericGoveeData {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java
new file mode 100644
index 0000000000000..24d0411dfa47b
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryData.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Govee Message - Device information
+ *
+ * @param ip IP address of the device
+ * @param device mac Address
+ * @param sku article number
+ * @param bleVersionHard Bluetooth HW version
+ * @param bleVersionSoft Bluetooth SW version
+ * @param wifiVersionHard Wifi HW version
+ * @param wifiVersionSoft Wife SW version
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record DiscoveryData(String ip, String device, String sku, String bleVersionHard, String bleVersionSoft,
+ String wifiVersionHard, String wifiVersionSoft) {
+ public DiscoveryData() {
+ this("", "", "", "", "", "", "");
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java
new file mode 100644
index 0000000000000..f5bf218a4934f
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Govee Message
+ *
+ * @param cmd
+ * @param data
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record DiscoveryMsg(String cmd, DiscoveryData data) {
+ public DiscoveryMsg() {
+ this("", new DiscoveryData());
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java
new file mode 100644
index 0000000000000..a1b7ae5b8cec8
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/DiscoveryResponse.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Govee Message
+ *
+ * @param msg message block
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record DiscoveryResponse(DiscoveryMsg msg) {
+ public DiscoveryResponse() {
+ this(new DiscoveryMsg());
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java
new file mode 100644
index 0000000000000..e04b4a0c174db
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/EmptyValueQueryStatusData.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Empty Govee Value Data
+ * Used to query device data
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record EmptyValueQueryStatusData() implements GenericGoveeData {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java
new file mode 100644
index 0000000000000..bd8f6af1658cd
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeData.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Govee Data Interface
+ *
+ * can hold different type of data content
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public interface GenericGoveeData {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java
new file mode 100644
index 0000000000000..89a5b31d5837d
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Generic Govee Data
+ *
+ * can hold different types of data with the command
+ *
+ * @param cmd
+ * @param data
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record GenericGoveeMsg(String cmd, GenericGoveeData data) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java
new file mode 100644
index 0000000000000..f8bb47b945fee
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/GenericGoveeRequest.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Govee Message
+ *
+ * @param msg message block
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record GenericGoveeRequest(GenericGoveeMsg msg) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java
new file mode 100644
index 0000000000000..33941a2e6b33c
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusData.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+/**
+ *
+ * @param onOff on=1 off=0
+ * @param brightness brightness
+ * @param color rgb color
+ * @param colorTemInKelvin color temperature in Kelvin
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+public record StatusData(int onOff, int brightness, Color color, int colorTemInKelvin) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java
new file mode 100644
index 0000000000000..8beb13388ee13
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+/**
+ * Govee Message - Cmd
+ *
+ * @param cmd Query Command
+ * @param data Status data
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+public record StatusMsg(String cmd, StatusData data) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java
new file mode 100644
index 0000000000000..19286ec5033c9
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/StatusResponse.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+/**
+ * Govee Message
+ *
+ * @param msg message block
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+public record StatusResponse(StatusMsg msg) {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java
new file mode 100644
index 0000000000000..81dcf25ef91ff
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueIntData.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Simple Govee Value Data
+ * typically used for On / Off
+ *
+ * @param value
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record ValueIntData(int value) implements GenericGoveeData {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java
new file mode 100644
index 0000000000000..08ca5a019fc09
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/model/ValueStringData.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ */
+package org.openhab.binding.govee.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Simple Govee Value Data
+ * typically used for On / Off
+ *
+ * @param value
+ *
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public record ValueStringData(String value) implements GenericGoveeData {
+}
diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..111e51af14da0
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,10 @@
+
+
+
+ binding
+ Govee Lan-API Binding
+ This is the binding for handling Govee Lights via the LAN-API interface.
+ local
+
diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 0000000000000..e153efa4d8067
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ network-address
+
+ Hostname or IP address of the device
+
+
+
+ MAC Address of the device
+
+
+
+ The amount of time that passes until the device is refreshed (in seconds)
+ 2
+
+
+
+
diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties
new file mode 100644
index 0000000000000..b600839cb40cf
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties
@@ -0,0 +1,80 @@
+# add-on
+
+addon.name = Govee Binding
+addon.description = This is the binding for handling Govee Lights via the LAN-API interface.
+
+# thing types
+
+thing-type.govee-light.label = Govee Light Thing
+thing-type.govee-light.description = Govee Light controllable via LAN API
+
+# thing types config
+
+thing-type.config.govee-light.refreshInterval.label = Light refresh interval (sec)
+thing-type.config.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed
+
+# product names
+
+discovery.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights
+discovery.govee-light.H6046 = H6046 RGBIC TV Light Bars
+discovery.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller
+discovery.govee-light.H6061 = H6061 Glide Hexa LED Panels
+discovery.govee-light.H6062 = H6062 Glide Wall Light
+discovery.govee-light.H6065 = H6065 Glide RGBIC Y Lights
+discovery.govee-light.H6066 = H6066 Glide Hexa Pro LED Panel
+discovery.govee-light.H6067 = H6067 Glide Triangle Light Panels
+discovery.govee-light.H6072 = H6072 RGBICWW Corner Floor Lamp
+discovery.govee-light.H6076 = H6076 RGBICW Smart Corner Floor Lamp
+discovery.govee-light.H6073 = H6073 LED Floor Lamp
+discovery.govee-light.H6078 = H6078 Cylinder Floor Lamp
+discovery.govee-light.H6087 = H6087 RGBIC Smart Wall Sconces
+discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights
+discovery.govee-light.H619A = H619A RGBIC Strip Lights With Protective Coating 5M
+discovery.govee-light.H619B = H619B RGBIC LED Strip Lights With Protective Coating
+discovery.govee-light.H619C = H619C LED Strip Lights With Protective Coating
+discovery.govee-light.H619D = H619D RGBIC PRO LED Strip Lights
+discovery.govee-light.H619E = H619E RGBIC LED Strip Lights With Protective Coating
+discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 1M
+discovery.govee-light.H61A1 = H61A1 RGBIC Neon Rope Light 2M
+discovery.govee-light.H61A2 = H61A2 RGBIC Neon Rope Light 5M
+discovery.govee-light.H61A3 = H61A3 RGBIC Neon Rope Light
+discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10
+discovery.govee-light.H61A8 = H61A8Neon Neon Rope Light 10
+discovery.govee-light.H618A = H618A RGBIC Basic LED Strip Lights 5M
+discovery.govee-light.H618C = H618C RGBIC Basic LED Strip Lights 5M
+discovery.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M
+discovery.govee-light.H6159 = H6159 RGB Light Strip
+discovery.govee-light.H615E = H615E LED Strip Lights 30M
+discovery.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M
+discovery.govee-light.H610A = H610A Glide Lively Wall Lights
+discovery.govee-light.H610B = H610B Music Wall Lights
+discovery.govee-light.H6172 = H6172 Outdoor LED Strip 10m
+discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight
+discovery.govee-light.H61E1 = H61E1 LED Strip Light M1
+discovery.govee-light.H7012 = H7012 Warm White Outdoor String Lights
+discovery.govee-light.H7013 = H7013 Warm White Outdoor String Lights
+discovery.govee-light.H7021 = H7021 RGBIC Warm White Smart Outdoor String
+discovery.govee-light.H7028 = H7028 Lynx Dream LED-Bulb String
+discovery.govee-light.H7041 = H7041 LED Outdoor Bulb String Lights
+discovery.govee-light.H7042 = H7042 LED Outdoor Bulb String Lights
+discovery.govee-light.H705A = H705A Permanent Outdoor Lights 30M
+discovery.govee-light.H705B = H705B Permanent Outdoor Lights 15M
+discovery.govee-light.H7050 = H7050 Outdoor Ground Lights 11M
+discovery.govee-light.H7051 = H7051 Outdoor Ground Lights 15M
+discovery.govee-light.H7055 = H7055 Pathway Light
+discovery.govee-light.H7060 = H7060 LED Flood Lights (2-Pack)
+discovery.govee-light.H7061 = H7061 LED Flood Lights (4-Pack)
+discovery.govee-light.H7062 = H7062 LED Flood Lights (6-Pack)
+discovery.govee-light.H7065 = H7065 Outdoor Spot Lights
+discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp
+discovery.govee-light.H6056 = H6056 H6056 Flow Plus
+discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids
+discovery.govee-light.H618F = H618F RGBIC LED Strip Lights
+discovery.govee-light.H618E = H618E LED Strip Lights 22m
+discovery.govee-light.H6168 = H6168 TV LED Backlight
+
+# thing status descriptions
+
+offline.communication-error.could-not-query-device = Could not control/query device at IP address {0}
+offline.configuration-error.ip-address.missing = IP address is missing
+offline.communication-error.empty-response = Empty response received
diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..002cb291f70c4
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ Govee light controllable via LAN API
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+ Controls the color temperature of the light in Kelvin
+ Temperature
+
+ Control
+ ColorTemperature
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java
new file mode 100644
index 0000000000000..01c73d6e3c710
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeDiscoveryTest.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Map;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.govee.internal.model.DiscoveryResponse;
+import org.openhab.core.config.discovery.DiscoveryResult;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeDiscoveryTest {
+
+ String response = """
+ {
+ "msg":{
+ "cmd":"scan",
+ "data":{
+ "ip":"192.168.178.171",
+ "device":"7D:31:C3:35:33:33:44:15",
+ "sku":"H6076",
+ "bleVersionHard":"3.01.01",
+ "bleVersionSoft":"1.04.04",
+ "wifiVersionHard":"1.00.10",
+ "wifiVersionSoft":"1.02.11"
+ }
+ }
+ }
+ """;
+
+ @Test
+ public void testProcessScanMessage() {
+ GoveeDiscoveryService service = new GoveeDiscoveryService(new CommunicationManager());
+ DiscoveryResponse resp = new Gson().fromJson(response, DiscoveryResponse.class);
+ Objects.requireNonNull(resp);
+ @Nullable
+ DiscoveryResult result = service.responseToResult(resp);
+ assertNotNull(result);
+ Map deviceProperties = result.getProperties();
+ assertEquals(deviceProperties.get(GoveeBindingConstants.DEVICE_TYPE), "H6076");
+ assertEquals(deviceProperties.get(GoveeBindingConstants.IP_ADDRESS), "192.168.178.171");
+ assertEquals(deviceProperties.get(GoveeBindingConstants.MAC_ADDRESS), "7D:31:C3:35:33:33:44:15");
+ }
+}
diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java
new file mode 100644
index 0000000000000..1aededf83e7f6
--- /dev/null
+++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeTest.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.govee.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.govee.internal.model.Color;
+import org.openhab.binding.govee.internal.model.ColorData;
+import org.openhab.binding.govee.internal.model.EmptyValueQueryStatusData;
+import org.openhab.binding.govee.internal.model.GenericGoveeMsg;
+import org.openhab.binding.govee.internal.model.GenericGoveeRequest;
+import org.openhab.binding.govee.internal.model.ValueIntData;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Stefan Höhn - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeSerializeTest {
+
+ private static final Gson GSON = new Gson();
+ private final String lightOffJsonString = "{\"msg\":{\"cmd\":\"turn\",\"data\":{\"value\":0}}}";
+ private final String lightOnJsonString = "{\"msg\":{\"cmd\":\"brightness\",\"data\":{\"value\":100}}}";
+ private final String lightColorJsonString = "{\"msg\":{\"cmd\":\"colorwc\",\"data\":{\"color\":{\"r\":0,\"g\":1,\"b\":2},\"colorTemInKelvin\":3}}}";
+ private final String lightBrightnessJsonString = "{\"msg\":{\"cmd\":\"brightness\",\"data\":{\"value\":99}}}";
+ private final String lightQueryJsonString = "{\"msg\":{\"cmd\":\"devStatus\",\"data\":{}}}";
+
+ @Test
+ public void testSerializeMessage() {
+ GenericGoveeRequest lightOff = new GenericGoveeRequest(new GenericGoveeMsg("turn", new ValueIntData(0)));
+ assertEquals(lightOffJsonString, GSON.toJson(lightOff));
+ GenericGoveeRequest lightOn = new GenericGoveeRequest(new GenericGoveeMsg("brightness", new ValueIntData(100)));
+ assertEquals(lightOnJsonString, GSON.toJson(lightOn));
+ GenericGoveeRequest lightColor = new GenericGoveeRequest(
+ new GenericGoveeMsg("colorwc", new ColorData(new Color(0, 1, 2), 3)));
+ assertEquals(lightColorJsonString, GSON.toJson(lightColor));
+ GenericGoveeRequest lightBrightness = new GenericGoveeRequest(
+ new GenericGoveeMsg("brightness", new ValueIntData(99)));
+ assertEquals(lightBrightnessJsonString, GSON.toJson(lightBrightness));
+ GenericGoveeRequest lightQuery = new GenericGoveeRequest(
+ new GenericGoveeMsg("devStatus", new EmptyValueQueryStatusData()));
+ assertEquals(lightQueryJsonString, GSON.toJson(lightQuery));
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index b58688b52dbfa..22f5c77c84628 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -153,6 +153,7 @@
org.openhab.binding.gceorg.openhab.binding.generacmobilelinkorg.openhab.binding.goecharger
+ org.openhab.binding.goveeorg.openhab.binding.gpioorg.openhab.binding.globalcacheorg.openhab.binding.gpstracker