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