mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Merge branch 'openhab:main' into 12773-stiebeleltron-enhancements
This commit is contained in:
commit
0edd04ba1f
@ -16,8 +16,6 @@ import java.io.Reader;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.script.Bindings;
|
||||
import javax.script.Compilable;
|
||||
import javax.script.CompiledScript;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
@ -37,7 +35,9 @@ import org.jruby.embed.jsr223.JRubyEngine;
|
||||
* @author Jimmy Tanagra - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JRubyEngineWrapper implements Compilable, Invocable, ScriptEngine {
|
||||
public class JRubyEngineWrapper implements Invocable, ScriptEngine {
|
||||
// Don't implement Compilable because there is a bug
|
||||
// in JRuby's compiled scripts: https://github.com/jruby/jruby/issues/8346
|
||||
|
||||
private final JRubyEngine engine;
|
||||
|
||||
@ -48,16 +48,6 @@ public class JRubyEngineWrapper implements Compilable, Invocable, ScriptEngine {
|
||||
this.engine = Objects.requireNonNull(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(@Nullable String script) throws ScriptException {
|
||||
return new JRubyCompiledScriptWrapper(engine.compile(script));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompiledScript compile(@Nullable Reader reader) throws ScriptException {
|
||||
return new JRubyCompiledScriptWrapper(engine.compile(reader));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object eval(@Nullable String script, @Nullable ScriptContext context) throws ScriptException {
|
||||
Object ctx = Objects.requireNonNull(context).getBindings(ScriptContext.ENGINE_SCOPE).get(CONTEXT_VAR_NAME);
|
||||
|
@ -429,9 +429,10 @@ public class FreeAtHomeBridgeHandler extends BaseBridgeHandler implements WebSoc
|
||||
|
||||
if (deviceHandler != null) {
|
||||
deviceHandler.onDeviceStateChanged(eventDatapointID, value);
|
||||
logger.debug("Socket event processed: event-datapoint-ID {} value {}", eventDatapointID, value);
|
||||
} else {
|
||||
logger.debug("Socket event not processed: event-datapoint-ID {} value {}", eventDatapointID, value);
|
||||
}
|
||||
|
||||
logger.debug("Socket event processed: event-datapoint-ID {} value {}", eventDatapointID, value);
|
||||
}
|
||||
}
|
||||
}
|
@ -291,7 +291,6 @@ public class FreeAtHomeDeviceHandler extends BaseThingHandler implements FreeAtH
|
||||
if (dpg == null) {
|
||||
logger.debug("Handle command for device (but invalid datapointgroup) {} - at channel {} - full command {}",
|
||||
device.getDeviceId(), channelUID.getAsString(), command.toFullString());
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/conf-error.invalid-deviceconfig");
|
||||
} else {
|
||||
@ -480,8 +479,7 @@ public class FreeAtHomeDeviceHandler extends BaseThingHandler implements FreeAtH
|
||||
channelTypeUID = createChannelTypeForDatapointgroup(dpg, channelTypeUID);
|
||||
}
|
||||
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, channel.getChannelId(),
|
||||
dpg.getLabel().substring(4));
|
||||
ChannelUID channelUID = createChannelUID(thingUID, channel.getChannelId(), dpg.getLabel());
|
||||
|
||||
String channelLabel = String.format("%s",
|
||||
i18nProvider.getText(bundle, dpg.getLabel(), "-", locale));
|
||||
@ -568,7 +566,7 @@ public class FreeAtHomeDeviceHandler extends BaseThingHandler implements FreeAtH
|
||||
channelTypeUID = createChannelTypeForDatapointgroup(dpg, channelTypeUID);
|
||||
}
|
||||
|
||||
ChannelUID channelUID = new ChannelUID(thingUID, channel.getChannelId());
|
||||
ChannelUID channelUID = createChannelUID(thingUID, channel.getChannelId(), dpg.getLabel());
|
||||
|
||||
FreeAtHomeDatapoint outputDatapoint = dpg.getOutputDatapoint();
|
||||
|
||||
@ -589,6 +587,11 @@ public class FreeAtHomeDeviceHandler extends BaseThingHandler implements FreeAtH
|
||||
}
|
||||
}
|
||||
|
||||
// Create a channel UID. Makes sure that the channel UID is unique and generated the same way every time
|
||||
private ChannelUID createChannelUID(ThingUID thingUID, String channelID, String dpgLabel) {
|
||||
return new ChannelUID(thingUID, channelID, dpgLabel.substring(4));
|
||||
}
|
||||
|
||||
public void removeChannels() {
|
||||
Bridge bridge = this.getBridge();
|
||||
|
@ -20,68 +20,133 @@ While Govee provides probably more than a hundred different lights, only the fol
|
||||
|
||||
Here is a list of the supported devices (the ones marked with * have been tested by the author)
|
||||
|
||||
- H619Z RGBIC Pro LED Strip Lights
|
||||
- H6042 Govee TV Light Bar #2
|
||||
- H6043 Govee TV Light Bars #2
|
||||
- H6046 RGBIC TV Light Bars
|
||||
- H6047 RGBIC Gaming Light Bars with Smart Controller
|
||||
- H6051 Aura - Smart Table Lamp
|
||||
- H6052 Govee Table Lamp
|
||||
- H6056 H6056 Flow Plus
|
||||
- H6059 RGBWW Night Light for Kids
|
||||
- H6061 Glide Hexa LED Panels (*)
|
||||
- H6062 Glide Wall Light
|
||||
- H6063 Gaming Wall Light
|
||||
- H6065 Glide RGBIC Y Lights
|
||||
- H6066 Glide Hexa Pro LED Panel
|
||||
- H6067 Glide Triangle Light Panels (*)
|
||||
- H606A Glide Hexa Light Panel Ultra
|
||||
- H6072 RGBICWW Corner Floor Lamp (*)
|
||||
- H6076 RGBICW Smart Corner Floor Lamp (*)
|
||||
- H6073 LED Floor Lamp
|
||||
- H6076 RGBICW Smart Corner Floor Lamp (*)
|
||||
- H6078 Cylinder Floor Lamp
|
||||
- H607C Floor Lamp #2
|
||||
- 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
|
||||
- H61C5 RGBIC LED Neon Rope Lights for Desks (*)
|
||||
- H61D3 Neon Rope Light 2 3M (*)
|
||||
- H61D5 Neon Rope Light 2 5M (*)
|
||||
- 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
|
||||
- H6088 RGBIC Cube Wall Sconces
|
||||
- H608A String Downlights 5M
|
||||
- H608B String Downlights 3M
|
||||
- H608C String Downlights 2M
|
||||
- H608D String Downlights 10M
|
||||
- H60A0 Ceiling Light
|
||||
- H60A1 Smart Ceiling Light (*)
|
||||
- H610A Glide Lively Wall Lights
|
||||
- H610B Music Wall Lights
|
||||
- H6110 2x5M Multicolor with Alexa
|
||||
- H6117 Dream Color LED Strip Light 10M
|
||||
- H6141 5M Smart Multicolor Strip Light
|
||||
- H6143 5M Strip Light
|
||||
- H6144 2x5M Strip Light
|
||||
- H6159 RGB Light Strip (*)
|
||||
- H615A 5M Light Strip with Alexa (*)
|
||||
- H615B 10M Light Strip with Alexa
|
||||
- H615C 15M Light Strip with Alexa
|
||||
- H615D 20M Light Strip with Alexa
|
||||
- H615E 30M Light Strip with Alexa
|
||||
- H6163 Dreamcolor LED Strip Light 5M
|
||||
- H6167 TV Backlight 2.4M
|
||||
- H6168 TV Backlight 2x0.7M+2x1.2M
|
||||
- H616C Outdoor Strip 10M
|
||||
- H616D Outdoor Strip 2x7.5M
|
||||
- H616E Outdoor Strip 2x10M
|
||||
- H6172 Outdoor LED Strip 10m
|
||||
- H61B2 RGBIC Neon TV Backlight
|
||||
- H6173 RGBIC Outdoor Strip Lights
|
||||
- H6175 RGBIC Outdoor Strip Lights 10M
|
||||
- H6176 RGBIC Outdoor Strip Lights 30M
|
||||
- H6182 WiFi Multicolor TV Strip Light
|
||||
- H618A RGBIC Basic LED Strip Lights 5M
|
||||
- H618C RGBIC Basic LED Strip Lights 5M
|
||||
- H618E LED Strip Lights 22m
|
||||
- H618F RGBIC LED Strip Lights
|
||||
- H619A Strip Lights With Protective Coating 5M
|
||||
- H619B Strip Lights With Protective Coating 7.5M
|
||||
- H619C Strip Lights With Protective Coating with Alexa 10M
|
||||
- H619D PRO LED Strip Lights with Alexa 2x7.5M
|
||||
- H619E Strip Lights With Protective Coating with Alexa 2x10M
|
||||
- H619Z Pro LED Strip Lights 3M
|
||||
- H61A0 RGBIC Neon Rope Light 3M
|
||||
- H61A1 RGBIC Neon Rope Light 2M
|
||||
- H61A2 RGBIC Neon Rope Light 5M
|
||||
- H61A3 RGBIC Neon Rope Light 4M
|
||||
- H61A5 Neon LED Strip Light 10M
|
||||
- H61A8 Neon Rope Light 10M
|
||||
- H61A8 Neon Rope Light 20M
|
||||
- H61B1 Strip Light with Cover 5M
|
||||
- H61B2 RGBIC Neon TV Backlight 3M
|
||||
- H61BA LED Strip Light 5M
|
||||
- H61BC LED Strip Light 10M
|
||||
- H61BE LED Strip Light 2x10M
|
||||
- H61C2 Neon LED Strip Light 2M
|
||||
- H61C2 Neon LED Strip Light 3M
|
||||
- H61C2 Neon LED Strip Light 5M
|
||||
- H61D3 Neon Rope Light 2 3m (*)
|
||||
- H61D5 Neon Rope Light 2 5m (*)
|
||||
- H61E0 LED Strip Light M1
|
||||
- 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
|
||||
- H7033 LED-Bulb String Lights
|
||||
- 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
|
||||
- H7052 Outdoor Ground Lights 15M
|
||||
- H7052 Outdoor Ground Lights 30M
|
||||
- H7055 Pathway Light
|
||||
- H705A Permanent Outdoor Lights 30M
|
||||
- H705B Permanent Outdoor Lights 15M
|
||||
- H705C Permanent Outdoor Lights 45M
|
||||
- H705D Permanent Outdoor Lights #2 15M
|
||||
- H705E Permanent Outdoor Lights #2 30M
|
||||
- H705F Permanent Outdoor Lights #2 45M
|
||||
- H7060 LED Flood Lights (2-Pack)
|
||||
- H7061 LED Flood Lights (4-Pack)
|
||||
- H7062 LED Flood Lights (6-Pack)
|
||||
- H7063 Outdoor Flood Lights
|
||||
- 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
|
||||
- H7066 Outdoor Spot Lights
|
||||
- H706A Permanent Outdoor Lights Pro 30M
|
||||
- H706B Permanent Outdoor Lights Pro 45M
|
||||
- H706C Permanent Outdoor Lights Pro 60M
|
||||
- H7070 Outdoor Projector Light (*)
|
||||
- H7075 Outdoor Wall Light
|
||||
- H70B1 520 LED Curtain Lights
|
||||
- H70BC 400 LED Curtain Lights
|
||||
- H70C1 RGBIC String Light 10M (*)
|
||||
- H70C2 RGBIC String Light 20M (*)
|
||||
- H805A Permanent Outdoor Lights Elite 30M
|
||||
- H805B Permanent Outdoor Lights Elite 15M
|
||||
- H805C Permanent Outdoor Lights Elite 45M
|
||||
|
||||
## Firewall
|
||||
|
||||
Govee devices communicate via multicast and unicast messages over the LAN.
|
||||
So you must ensure that any firewall on your openHAB server is configured to pass the following traffic:
|
||||
|
||||
- Multicast UDP on 239.255.255.250 port 4001
|
||||
- Incoming unicast UDP on port 4002
|
||||
- Outgoing unicast UDP on port 4003
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery is done by scanning the devices in the Thing section.
|
||||
@ -108,11 +173,13 @@ 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 |
|
||||
| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
|
||||
| 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 |
|
||||
| refreshInterval | integer | Interval the device is polled in sec. | 5 | no | yes |
|
||||
| minKelvin | integer | The minimum color temperature that the light supports in Kelvin. | N/A | no | yes |
|
||||
| maxKelvin | integer | The maximum color temperature that the light supports in Kelvin. | N/A | no | yes |
|
||||
|
||||
## Channels
|
||||
|
||||
|
@ -15,13 +15,23 @@ package org.openhab.binding.govee.internal;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MulticastSocket;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.StandardSocketOptions;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -29,6 +39,7 @@ 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.osgi.service.component.annotations.Deactivate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -36,231 +47,259 @@ 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.
|
||||
* The {@link CommunicationManager} component implements a sender to send commands to Govee devices,
|
||||
* and implements a thread that handles the notifications from all devices. It applies the status
|
||||
* information to the right Thing. It supports both discovery and status commands and notifications
|
||||
* concurrently.
|
||||
*
|
||||
* @author Stefan Höhn - Initial contribution
|
||||
* @author Danny Baumann - Thread-Safe design refactoring
|
||||
* @author Andrew Fiddian-Green - New threading model using java.nio channel
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = CommunicationManager.class)
|
||||
public class CommunicationManager {
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
|
||||
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;
|
||||
|
||||
// list of Thing handler listeners that will receive state notifications
|
||||
private final Map<String, GoveeHandler> thingHandlerListeners = new ConcurrentHashMap<>();
|
||||
|
||||
private @Nullable GoveeDiscoveryListener discoveryListener;
|
||||
private @Nullable Thread serverThread;
|
||||
private boolean serverStopFlag = false;
|
||||
|
||||
private final Object paramsLock = new Object();
|
||||
private final Object serverLock = new Object();
|
||||
private final Object senderLock = new Object();
|
||||
|
||||
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;
|
||||
public static final int SCAN_TIMEOUT_SEC = 5;
|
||||
|
||||
private static final String DISCOVER_REQUEST = "{\"msg\": {\"cmd\": \"scan\", \"data\": {\"account_topic\": \"reserve\"}}}";
|
||||
|
||||
public interface DiscoveryResultReceiver {
|
||||
void onResultReceived(DiscoveryResponse result);
|
||||
private static final InetSocketAddress DISCOVERY_SOCKET_ADDRESS = new InetSocketAddress(DISCOVERY_MULTICAST_ADDRESS,
|
||||
DISCOVERY_PORT);
|
||||
|
||||
public interface GoveeDiscoveryListener {
|
||||
void onDiscoveryResponse(DiscoveryResponse discoveryResponse);
|
||||
}
|
||||
|
||||
@Activate
|
||||
public CommunicationManager() {
|
||||
serverStart();
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
thingHandlerListeners.clear();
|
||||
discoveryListener = null;
|
||||
serverStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Thing handlers register themselves to receive state updates when they are initialized.
|
||||
*/
|
||||
public void registerHandler(GoveeHandler handler) {
|
||||
synchronized (thingHandlers) {
|
||||
thingHandlers.put(handler.getHostname(), handler);
|
||||
if (receiverThread == null) {
|
||||
receiverThread = new StatusReceiver();
|
||||
receiverThread.start();
|
||||
}
|
||||
}
|
||||
thingHandlerListeners.put(ipAddressFrom(handler.getHostname()), handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thing handlers unregister themselves when they are destroyed.
|
||||
*/
|
||||
public void unregisterHandler(GoveeHandler handler) {
|
||||
synchronized (thingHandlers) {
|
||||
thingHandlers.remove(handler.getHostname());
|
||||
if (thingHandlers.isEmpty()) {
|
||||
StatusReceiver receiver = receiverThread;
|
||||
if (receiver != null) {
|
||||
receiver.stopReceiving();
|
||||
}
|
||||
receiverThread = null;
|
||||
}
|
||||
}
|
||||
thingHandlerListeners.remove(ipAddressFrom(handler.getHostname()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a unicast command request to the device.
|
||||
*/
|
||||
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.trace("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();
|
||||
}
|
||||
serverStart();
|
||||
synchronized (senderLock) {
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
socket.setReuseAddress(true);
|
||||
String message = gson.toJson(request);
|
||||
byte[] data = message.getBytes();
|
||||
String hostname = handler.getHostname();
|
||||
InetAddress address = InetAddress.getByName(hostname);
|
||||
DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
|
||||
socket.send(packet);
|
||||
logger.trace("Sent request to {} on {} with content = {}", handler.getThing().getUID(),
|
||||
address.getHostAddress(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StatusReceiver extends Thread {
|
||||
private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
|
||||
private boolean stopped = false;
|
||||
private @Nullable DiscoveryResultReceiver discoveryResultReceiver;
|
||||
/**
|
||||
* Send discovery multicast pings on any ipv4 address bound to any network interface in the given
|
||||
* list and then sleep for sufficient time until responses may have been received.
|
||||
*/
|
||||
public void runDiscoveryForInterfaces(List<NetworkInterface> interfaces, GoveeDiscoveryListener listener) {
|
||||
serverStart();
|
||||
try {
|
||||
discoveryListener = listener;
|
||||
Instant sleepUntil = Instant.now().plusSeconds(SCAN_TIMEOUT_SEC);
|
||||
|
||||
private @Nullable MulticastSocket socket;
|
||||
interfaces.parallelStream() // send on all interfaces in parallel
|
||||
.forEach(interFace -> Collections.list(interFace.getInetAddresses()).stream()
|
||||
.filter(address -> address instanceof Inet4Address).map(address -> address.getHostAddress())
|
||||
.forEach(ipv4Address -> sendPing(interFace, ipv4Address)));
|
||||
|
||||
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) {
|
||||
Duration sleepDuration = Duration.between(Instant.now(), sleepUntil);
|
||||
if (!sleepDuration.isNegative()) {
|
||||
try {
|
||||
socket = new MulticastSocket(RESPONSE_PORT);
|
||||
byte[] buffer = new byte[10240];
|
||||
socket.setReuseAddress(true);
|
||||
while (!stopped) {
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
if (!socket.isClosed()) {
|
||||
socket.receive(packet);
|
||||
} else {
|
||||
logger.warn("Socket was unexpectedly closed");
|
||||
break;
|
||||
}
|
||||
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) {
|
||||
logger.debug(
|
||||
"JsonParseException when trying to parse the response, probably a status message",
|
||||
e);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
Thread.sleep(sleepDuration.toMillis());
|
||||
} catch (InterruptedException e) {
|
||||
// just return
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
discoveryListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets executed on the server thread. It uses a {@link DatagramChannel} to listen on port
|
||||
* 4002 and it processes any notifications received. The task runs continuously in a loop until the
|
||||
* thread is externally interrupted.
|
||||
*
|
||||
* <li>In case of status notifications it forwards the message to the Thing handler listener.</li>
|
||||
* <li>In case of discovery notifications it forwards the message to the discovery listener.</li>
|
||||
* <li>If there is neither a Thing handler listener, nor a discovery listener, it logs an error.</li>
|
||||
*/
|
||||
private void serverThreadTask() {
|
||||
synchronized (serverLock) {
|
||||
try {
|
||||
logger.trace("Server thread started.");
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
|
||||
while (!serverStopFlag) {
|
||||
try (DatagramChannel channel = DatagramChannel.open()
|
||||
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
|
||||
.bind(new InetSocketAddress(RESPONSE_PORT))) {
|
||||
|
||||
while (!serverStopFlag) {
|
||||
String sourceIp = "";
|
||||
try {
|
||||
SocketAddress socketAddress = channel.receive(buffer.clear());
|
||||
if ((socketAddress instanceof InetSocketAddress inetSocketAddress)
|
||||
&& (inetSocketAddress.getAddress() instanceof InetAddress inetAddress)) {
|
||||
sourceIp = inetAddress.getHostAddress();
|
||||
} else {
|
||||
logger.debug("Receive() - bad socketAddress={}", socketAddress);
|
||||
return;
|
||||
}
|
||||
} catch (ClosedByInterruptException e) {
|
||||
// thrown if 'Thread.interrupt()' is called during 'channel.receive()'
|
||||
logger.debug("Receive ClosedByInterruptException, isInterrupted={}, serverStopFlag={}",
|
||||
Thread.currentThread().isInterrupted(), serverStopFlag);
|
||||
Thread.interrupted(); // clear 'interrupted' flag
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
logger.debug("Receive unexpected exception={}", e.getMessage());
|
||||
break;
|
||||
}
|
||||
|
||||
String message = new String(buffer.array(), 0, buffer.position());
|
||||
logger.trace("Receive from sourceIp={}, message={}", sourceIp, message);
|
||||
|
||||
GoveeHandler handler = thingHandlerListeners.get(sourceIp);
|
||||
boolean devStatus = message.contains("devStatus");
|
||||
if (handler != null && devStatus) {
|
||||
logger.debug("Notifying status of thing={} on sourcecIp={}",
|
||||
handler.getThing().getUID(), sourceIp);
|
||||
handler.handleIncomingStatus(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
GoveeDiscoveryListener discoveryListener = this.discoveryListener;
|
||||
if (!devStatus && discoveryListener != null) {
|
||||
try {
|
||||
DiscoveryResponse response = gson.fromJson(message, DiscoveryResponse.class);
|
||||
if (response != null) {
|
||||
logger.debug("Notifying discovery of device on sourceIp={}", sourceIp);
|
||||
discoveryListener.onDiscoveryResponse(response);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Discovery notification parse exception={}", e.getMessage());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
"Unhandled message with sourceIp={}, devStatus={}, handler={}, discoveryListener={}",
|
||||
sourceIp, devStatus, handler, discoveryListener);
|
||||
} // end of inner while loop
|
||||
} catch (IOException e) {
|
||||
logger.debug("Datagram channel create exception={}", e.getMessage());
|
||||
}
|
||||
} // end of outer while loop
|
||||
} finally {
|
||||
serverThread = null;
|
||||
serverStopFlag = false;
|
||||
logger.trace("Server thread terminated.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved IP address from the given host name.
|
||||
*/
|
||||
private static String ipAddressFrom(String host) {
|
||||
try {
|
||||
return InetAddress.getByName(host).getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the server thread if it is not already running.
|
||||
*/
|
||||
private void serverStart() {
|
||||
synchronized (paramsLock) {
|
||||
Thread serverthread = serverThread;
|
||||
if (serverthread == null) {
|
||||
serverthread = new Thread(this::serverThreadTask, "OH-binding-" + GoveeBindingConstants.BINDING_ID);
|
||||
serverThread = serverthread;
|
||||
serverStopFlag = false;
|
||||
serverthread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the server thread.
|
||||
*/
|
||||
private void serverStop() {
|
||||
synchronized (paramsLock) {
|
||||
serverStopFlag = true;
|
||||
Thread serverthread = serverThread;
|
||||
if (serverthread != null) {
|
||||
serverthread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send discovery ping multicast on the given network interface and ipv4 address.
|
||||
*/
|
||||
private void sendPing(NetworkInterface interFace, String ipv4Address) {
|
||||
try (DatagramChannel channel = (DatagramChannel) DatagramChannel.open(StandardProtocolFamily.INET)
|
||||
.setOption(StandardSocketOptions.SO_REUSEADDR, true)
|
||||
.setOption(StandardSocketOptions.IP_MULTICAST_TTL, 64)
|
||||
.setOption(StandardSocketOptions.IP_MULTICAST_IF, interFace)
|
||||
.bind(new InetSocketAddress(ipv4Address, DISCOVERY_PORT)).configureBlocking(false)) {
|
||||
channel.send(ByteBuffer.wrap(DISCOVER_REQUEST.getBytes()), DISCOVERY_SOCKET_ADDRESS);
|
||||
logger.trace("Sent ping from {}:{} ({}) to {}:{} with content = {}", ipv4Address, DISCOVERY_PORT,
|
||||
interFace.getDisplayName(), DISCOVERY_MULTICAST_ADDRESS, DISCOVERY_PORT, DISCOVER_REQUEST);
|
||||
} catch (IOException e) {
|
||||
logger.debug("Network error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ public class GoveeBindingConstants {
|
||||
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";
|
||||
public 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");
|
||||
@ -44,4 +44,7 @@ public class GoveeBindingConstants {
|
||||
// Limit values of channels
|
||||
public static final Double COLOR_TEMPERATURE_MIN_VALUE = 2000.0;
|
||||
public static final Double COLOR_TEMPERATURE_MAX_VALUE = 9000.0;
|
||||
|
||||
public static final String PROPERTY_COLOR_TEMPERATURE_MIN = "minKelvin";
|
||||
public static final String PROPERTY_COLOR_TEMPERATURE_MAX = "maxKelvin";
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
package org.openhab.binding.govee.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link GoveeConfiguration} contains thing values that are used by the Thing Handler
|
||||
@ -24,4 +25,7 @@ public class GoveeConfiguration {
|
||||
|
||||
public String hostname = "";
|
||||
public int refreshInterval = 5; // in seconds
|
||||
|
||||
public @Nullable Integer minKelvin;
|
||||
public @Nullable Integer maxKelvin;
|
||||
}
|
||||
|
@ -12,16 +12,18 @@
|
||||
*/
|
||||
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 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.CommunicationManager.GoveeDiscoveryListener;
|
||||
import org.openhab.binding.govee.internal.model.DiscoveryData;
|
||||
import org.openhab.binding.govee.internal.model.DiscoveryResponse;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
@ -79,17 +81,22 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.govee")
|
||||
public class GoveeDiscoveryService extends AbstractDiscoveryService {
|
||||
public class GoveeDiscoveryService extends AbstractDiscoveryService implements GoveeDiscoveryListener {
|
||||
|
||||
private static final int BACKGROUND_SCAN_INTERVAL_SECONDS = 300;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GoveeDiscoveryService.class);
|
||||
|
||||
private CommunicationManager communicationManager;
|
||||
private final CommunicationManager communicationManager;
|
||||
private @Nullable ScheduledFuture<?> backgroundScanTask;
|
||||
|
||||
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);
|
||||
public GoveeDiscoveryService(final @Reference TranslationProvider i18nProvider,
|
||||
final @Reference LocaleProvider localeProvider,
|
||||
final @Reference CommunicationManager communicationManager) {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, CommunicationManager.SCAN_TIMEOUT_SEC, true);
|
||||
this.i18nProvider = i18nProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
this.communicationManager = communicationManager;
|
||||
@ -103,23 +110,8 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
@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");
|
||||
});
|
||||
logger.debug("Starting scan");
|
||||
scheduler.schedule(this::doDiscovery, 0, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public @Nullable DiscoveryResult responseToResult(DiscoveryResponse response) {
|
||||
@ -165,11 +157,11 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
|
||||
}
|
||||
|
||||
String hwVersion = data.wifiVersionHard();
|
||||
if (hwVersion != null) {
|
||||
if (!hwVersion.isEmpty()) {
|
||||
builder.withProperty(GoveeBindingConstants.HW_VERSION, hwVersion);
|
||||
}
|
||||
String swVersion = data.wifiVersionSoft();
|
||||
if (swVersion != null) {
|
||||
if (!swVersion.isEmpty()) {
|
||||
builder.withProperty(GoveeBindingConstants.SW_VERSION, swVersion);
|
||||
}
|
||||
|
||||
@ -194,4 +186,41 @@ public class GoveeDiscoveryService extends AbstractDiscoveryService {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command the {@link CommunicationManager) to run the scans.
|
||||
*/
|
||||
private void doDiscovery() {
|
||||
communicationManager.runDiscoveryForInterfaces(getLocalNetworkInterfaces(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called back by the {@link CommunicationManager} when it receives a {@link DiscoveryResponse}
|
||||
* notification carrying information about potential newly discovered Things.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void onDiscoveryResponse(DiscoveryResponse discoveryResponse) {
|
||||
DiscoveryResult discoveryResult = responseToResult(discoveryResponse);
|
||||
if (discoveryResult != null) {
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
ScheduledFuture<?> backgroundScanTask = this.backgroundScanTask;
|
||||
if (backgroundScanTask == null || backgroundScanTask.isCancelled()) {
|
||||
this.backgroundScanTask = scheduler.scheduleWithFixedDelay(this::doDiscovery, 0,
|
||||
BACKGROUND_SCAN_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
ScheduledFuture<?> backgroundScanTask = this.backgroundScanTask;
|
||||
if (backgroundScanTask != null) {
|
||||
backgroundScanTask.cancel(true);
|
||||
this.backgroundScanTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ package org.openhab.binding.govee.internal;
|
||||
import static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -24,10 +29,12 @@ 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.GenericGoveeData;
|
||||
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.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
@ -40,6 +47,7 @@ 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.types.UnDefType;
|
||||
import org.openhab.core.util.ColorUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -71,47 +79,73 @@ import com.google.gson.JsonSyntaxException;
|
||||
* https://app-h5.govee.com/user-manual/wlan-guide
|
||||
*
|
||||
* @author Stefan Höhn - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Added sequential task processing
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeHandler extends BaseThingHandler {
|
||||
|
||||
/*
|
||||
* Messages to be sent to the Govee devices
|
||||
*/
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final int REFRESH_SECONDS_MIN = 2;
|
||||
private static final int INTER_COMMAND_DELAY_MILLISEC = 100;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
|
||||
|
||||
protected ScheduledExecutorService executorService = scheduler;
|
||||
@Nullable
|
||||
private ScheduledFuture<?> triggerStatusJob; // send device status update job
|
||||
private @Nullable ScheduledFuture<?> thingTaskSenderTask;
|
||||
private GoveeConfiguration goveeConfiguration = new GoveeConfiguration();
|
||||
|
||||
private CommunicationManager communicationManager;
|
||||
private final CommunicationManager communicationManager;
|
||||
private final GoveeStateDescriptionProvider stateDescriptionProvider;
|
||||
private final List<Callable<Boolean>> taskQueue = new ArrayList<>();
|
||||
|
||||
private int lastOnOff;
|
||||
private int lastBrightness;
|
||||
private OnOffType lastSwitch = OnOffType.OFF;
|
||||
private HSBType lastColor = new HSBType();
|
||||
private int lastColorTempInKelvin = COLOR_TEMPERATURE_MIN_VALUE.intValue();
|
||||
|
||||
private int lastKelvin;
|
||||
private int minKelvin;
|
||||
private int maxKelvin;
|
||||
|
||||
private int refreshIntervalSeconds;
|
||||
private Instant nextRefreshDueTime = Instant.EPOCH;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* This thing related job <i>thingTaskSender</i> sends the next queued command (if any)
|
||||
* to the Govee device. If there is no queued command and a regular refresh is due then
|
||||
* sends the command to trigger a status refresh.
|
||||
*
|
||||
* The device may send a reply to the common port and if so the response is received by
|
||||
* the refresh status receiver.
|
||||
*/
|
||||
private final Runnable thingRefreshSender = () -> {
|
||||
try {
|
||||
triggerDeviceStatusRefresh();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
|
||||
+ "\"]");
|
||||
private final Runnable thingTaskSender = () -> {
|
||||
synchronized (taskQueue) {
|
||||
if (taskQueue.isEmpty() && Instant.now().isBefore(nextRefreshDueTime)) {
|
||||
return; // no queued command nor pending refresh
|
||||
}
|
||||
if (taskQueue.isEmpty()) {
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
nextRefreshDueTime = Instant.now().plusSeconds(refreshIntervalSeconds);
|
||||
} else if (taskQueue.size() > 20) {
|
||||
logger.info("Command task queue size:{} exceeds limit:20", taskQueue.size());
|
||||
}
|
||||
try {
|
||||
if (taskQueue.remove(0).call()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
logger.warn("Unexpected List.remove() exception:{}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
|
||||
+ "\"]");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public GoveeHandler(Thing thing, CommunicationManager communicationManager) {
|
||||
public GoveeHandler(Thing thing, CommunicationManager communicationManager,
|
||||
GoveeStateDescriptionProvider stateDescriptionProvider) {
|
||||
super(thing);
|
||||
this.communicationManager = communicationManager;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
@ -128,140 +162,176 @@ public class GoveeHandler extends BaseThingHandler {
|
||||
"@text/offline.configuration-error.ip-address.missing");
|
||||
return;
|
||||
}
|
||||
|
||||
minKelvin = Objects.requireNonNullElse(goveeConfiguration.minKelvin, COLOR_TEMPERATURE_MIN_VALUE.intValue());
|
||||
maxKelvin = Objects.requireNonNullElse(goveeConfiguration.maxKelvin, COLOR_TEMPERATURE_MAX_VALUE.intValue());
|
||||
if ((minKelvin < COLOR_TEMPERATURE_MIN_VALUE) || (maxKelvin > COLOR_TEMPERATURE_MAX_VALUE)
|
||||
|| (minKelvin >= maxKelvin)) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.configuration-error.invalid-color-temperature-range");
|
||||
return;
|
||||
}
|
||||
|
||||
thing.setProperty(PROPERTY_COLOR_TEMPERATURE_MIN, Integer.toString(minKelvin));
|
||||
thing.setProperty(PROPERTY_COLOR_TEMPERATURE_MAX, Integer.toString(maxKelvin));
|
||||
stateDescriptionProvider.setMinMaxKelvin(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE_ABS),
|
||||
minKelvin, maxKelvin);
|
||||
|
||||
refreshIntervalSeconds = goveeConfiguration.refreshInterval;
|
||||
if (refreshIntervalSeconds < REFRESH_SECONDS_MIN) {
|
||||
logger.warn("Config Param refreshInterval={} too low, minimum={}", refreshIntervalSeconds,
|
||||
REFRESH_SECONDS_MIN);
|
||||
refreshIntervalSeconds = REFRESH_SECONDS_MIN;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
communicationManager.registerHandler(this);
|
||||
if (triggerStatusJob == null) {
|
||||
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
|
||||
|
||||
triggerStatusJob = executorService.scheduleWithFixedDelay(thingRefreshSender, 100,
|
||||
goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS);
|
||||
if (thingTaskSenderTask == null) {
|
||||
logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
|
||||
thingTaskSenderTask = executorService.scheduleWithFixedDelay(thingTaskSender, INTER_COMMAND_DELAY_MILLISEC,
|
||||
INTER_COMMAND_DELAY_MILLISEC, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
|
||||
ScheduledFuture<?> triggerStatusJobFuture = triggerStatusJob;
|
||||
if (triggerStatusJobFuture != null) {
|
||||
triggerStatusJobFuture.cancel(true);
|
||||
triggerStatusJob = null;
|
||||
taskQueue.clear();
|
||||
ScheduledFuture<?> job = thingTaskSenderTask;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
thingTaskSenderTask = null;
|
||||
}
|
||||
communicationManager.unregisterHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
try {
|
||||
public void handleCommand(ChannelUID channelUID, Command commandParam) {
|
||||
Command command = commandParam;
|
||||
|
||||
synchronized (taskQueue) {
|
||||
logger.debug("handleCommand({}, {})", channelUID, command);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
// we are refreshing all channels at once, as we get all information at the same time
|
||||
triggerDeviceStatusRefresh();
|
||||
logger.debug("Triggering Refresh");
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
} 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);
|
||||
if (command instanceof HSBType hsb) {
|
||||
taskQueue.add(() -> sendColor(hsb));
|
||||
command = hsb.getBrightness(); // fall through
|
||||
}
|
||||
if (command instanceof PercentType percent) {
|
||||
taskQueue.add(() -> sendBrightness(percent));
|
||||
command = OnOffType.from(percent.intValue() > 0); // fall through
|
||||
}
|
||||
if (command instanceof OnOffType onOff) {
|
||||
taskQueue.add(() -> sendOnOff(onOff));
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
}
|
||||
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);
|
||||
taskQueue.add(() -> sendKelvin(percentToKelvin(percent)));
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
}
|
||||
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);
|
||||
if (command instanceof QuantityType<?> genericQuantity) {
|
||||
QuantityType<?> kelvin = genericQuantity.toInvertibleUnit(Units.KELVIN);
|
||||
if (kelvin == null) {
|
||||
logger.warn("handleCommand() invalid QuantityType:{}", genericQuantity);
|
||||
break;
|
||||
}
|
||||
taskQueue.add(() -> sendKelvin(kelvin.intValue()));
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
} else if (command instanceof DecimalType kelvin) {
|
||||
taskQueue.add(() -> sendKelvin(kelvin.intValue()));
|
||||
taskQueue.add(() -> triggerDeviceStatusRefresh());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
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
|
||||
*
|
||||
* Initiate a refresh to our thing device
|
||||
*/
|
||||
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);
|
||||
private boolean triggerDeviceStatusRefresh() throws IOException {
|
||||
logger.debug("triggerDeviceStatusRefresh() to {}", thing.getUID());
|
||||
GenericGoveeData data = new EmptyValueQueryStatusData();
|
||||
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("devStatus", data));
|
||||
communicationManager.sendRequest(this, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Send the normalized RGB color parameters.
|
||||
*/
|
||||
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);
|
||||
public boolean sendColor(HSBType color) throws IOException {
|
||||
logger.debug("sendColor({}) to {}", color, thing.getUID());
|
||||
int[] normalRGB = ColorUtil.hsbToRgb(new HSBType(color.getHue(), color.getSaturation(), PercentType.HUNDRED));
|
||||
GenericGoveeData data = new ColorData(new Color(normalRGB[0], normalRGB[1], normalRGB[2]), 0);
|
||||
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", data));
|
||||
communicationManager.sendRequest(this, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
void handleIncomingStatus(String response) {
|
||||
/**
|
||||
* Send the brightness parameter.
|
||||
*/
|
||||
public boolean sendBrightness(PercentType brightness) throws IOException {
|
||||
logger.debug("sendBrightness({}) to {}", brightness, thing.getUID());
|
||||
GenericGoveeData data = new ValueIntData(brightness.intValue());
|
||||
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("brightness", data));
|
||||
communicationManager.sendRequest(this, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the on-off parameter.
|
||||
*/
|
||||
private boolean sendOnOff(OnOffType onOff) throws IOException {
|
||||
logger.debug("sendOnOff({}) to {}", onOff, thing.getUID());
|
||||
GenericGoveeData data = new ValueIntData(onOff == OnOffType.ON ? 1 : 0);
|
||||
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("turn", data));
|
||||
communicationManager.sendRequest(this, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the color temperature (Kelvin) parameter.
|
||||
*/
|
||||
private boolean sendKelvin(int kelvin) throws IOException {
|
||||
logger.debug("sendKelvin({}) to {}", kelvin, thing.getUID());
|
||||
GenericGoveeData data = new ColorData(new Color(0, 0, 0), kelvin);
|
||||
GenericGoveeRequest request = new GenericGoveeRequest(new GenericGoveeMsg("colorwc", data));
|
||||
communicationManager.sendRequest(this, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an {@link HSBType} from the given normalized {@link Color} RGB parameters, brightness, and on-off state
|
||||
* parameters. If the on parameter is true then use the brightness parameter, otherwise use a brightness of zero.
|
||||
*
|
||||
* @param normalRgbParams record containing the lamp's normalized RGB parameters (0..255)
|
||||
* @param brightnessParam the lamp brightness in range 0..100
|
||||
* @param onParam the lamp on-off state
|
||||
*
|
||||
* @return the respective HSBType
|
||||
*/
|
||||
private static HSBType buildHSB(Color normalRgbParams, int brightnessParam, boolean onParam) {
|
||||
HSBType normalColor = ColorUtil
|
||||
.rgbToHsb(new int[] { normalRgbParams.r(), normalRgbParams.g(), normalRgbParams.b() });
|
||||
PercentType brightness = onParam ? new PercentType(brightnessParam) : PercentType.ZERO;
|
||||
return new HSBType(normalColor.getHue(), normalColor.getSaturation(), brightness);
|
||||
}
|
||||
|
||||
public void handleIncomingStatus(String response) {
|
||||
if (response.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/offline.communication-error.empty-response");
|
||||
@ -284,47 +354,52 @@ public class GoveeHandler extends BaseThingHandler {
|
||||
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);
|
||||
logger.debug("updateDeviceState() for {}", thing.getUID());
|
||||
|
||||
newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE)
|
||||
? COLOR_TEMPERATURE_MIN_VALUE.intValue()
|
||||
: newColorTempInKelvin;
|
||||
newColorTempInKelvin = (newColorTempInKelvin > COLOR_TEMPERATURE_MAX_VALUE)
|
||||
? COLOR_TEMPERATURE_MAX_VALUE.intValue()
|
||||
: newColorTempInKelvin;
|
||||
OnOffType sw = OnOffType.from(message.msg().data().onOff() == 1);
|
||||
int brightness = message.msg().data().brightness();
|
||||
Color normalRGB = message.msg().data().color();
|
||||
int kelvin = message.msg().data().colorTemInKelvin();
|
||||
|
||||
int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE)
|
||||
/ (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
|
||||
logger.trace("Update values: switch:{}, brightness:{}, normalRGB:{}, kelvin:{}", sw, brightness, normalRGB,
|
||||
kelvin);
|
||||
|
||||
HSBType adaptedColor = getColorState(newColor, newBrightness);
|
||||
HSBType color = buildHSB(normalRGB, brightness, true);
|
||||
|
||||
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);
|
||||
logger.trace("Compare hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
|
||||
if ((sw != lastSwitch) || !color.equals(lastColor)) {
|
||||
logger.trace("Update hsb old:{} to new:{}, switch old:{} to new:{}", lastColor, color, lastSwitch, sw);
|
||||
updateState(CHANNEL_COLOR, buildHSB(normalRGB, brightness, sw == OnOffType.ON));
|
||||
lastSwitch = sw;
|
||||
lastColor = color;
|
||||
}
|
||||
|
||||
// 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));
|
||||
logger.trace("Compare kelvin old:{} to new:{}", lastKelvin, kelvin);
|
||||
if (kelvin != lastKelvin) {
|
||||
logger.trace("Update kelvin old:{} to new:{}", lastKelvin, kelvin);
|
||||
if (kelvin != 0) {
|
||||
kelvin = Math.round(Math.min(maxKelvin, Math.max(minKelvin, kelvin)));
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE, kelvinToPercent(kelvin));
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, QuantityType.valueOf(kelvin, Units.KELVIN));
|
||||
} else {
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE, UnDefType.UNDEF);
|
||||
updateState(CHANNEL_COLOR_TEMPERATURE_ABS, UnDefType.UNDEF);
|
||||
}
|
||||
lastKelvin = kelvin;
|
||||
}
|
||||
}
|
||||
|
||||
lastOnOff = newOnOff;
|
||||
lastColor = adaptedColor;
|
||||
lastBrightness = newBrightness;
|
||||
/**
|
||||
* Convert PercentType to Kelvin.
|
||||
*/
|
||||
private int percentToKelvin(PercentType percent) {
|
||||
return (int) Math.round((((maxKelvin - minKelvin) * percent.doubleValue() / 100.0) + minKelvin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Kelvin to PercentType.
|
||||
*/
|
||||
private PercentType kelvinToPercent(int kelvin) {
|
||||
return new PercentType((int) Math.round((kelvin - minKelvin) * 100.0 / (maxKelvin - minKelvin)));
|
||||
}
|
||||
}
|
||||
|
@ -38,11 +38,14 @@ import org.osgi.service.component.annotations.Reference;
|
||||
public class GoveeHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_LIGHT);
|
||||
|
||||
private CommunicationManager communicationManager;
|
||||
private final CommunicationManager communicationManager;
|
||||
private final GoveeStateDescriptionProvider stateDescriptionProvider;
|
||||
|
||||
@Activate
|
||||
public GoveeHandlerFactory(@Reference CommunicationManager communicationManager) {
|
||||
public GoveeHandlerFactory(final @Reference CommunicationManager communicationManager,
|
||||
final @Reference GoveeStateDescriptionProvider stateDescriptionProvider) {
|
||||
this.communicationManager = communicationManager;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -55,7 +58,7 @@ public class GoveeHandlerFactory extends BaseThingHandlerFactory {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_LIGHT.equals(thingTypeUID)) {
|
||||
return new GoveeHandler(thing, communicationManager);
|
||||
return new GoveeHandler(thing, communicationManager, stateDescriptionProvider);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.events.ThingEventFactory;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.openhab.core.types.StateDescription;
|
||||
import org.openhab.core.types.StateDescriptionFragment;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link GoveeStateDescriptionProvider} provides state descriptions for different color temperature ranges.
|
||||
*
|
||||
* @author Andrew Fiddian-Green - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, GoveeStateDescriptionProvider.class })
|
||||
public class GoveeStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
|
||||
private final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>();
|
||||
|
||||
@Activate
|
||||
public GoveeStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
|
||||
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
|
||||
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
|
||||
@Nullable Locale locale) {
|
||||
StateDescriptionFragment stateDescriptionFragment = stateDescriptionFragments.get(channel.getUID());
|
||||
return stateDescriptionFragment != null ? stateDescriptionFragment.toStateDescription()
|
||||
: super.getStateDescription(channel, original, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state description minimum and maximum values and pattern in Kelvin for the given channel UID
|
||||
*/
|
||||
public void setMinMaxKelvin(ChannelUID channelUID, long minKelvin, long maxKelvin) {
|
||||
StateDescriptionFragment oldStateDescriptionFragment = stateDescriptionFragments.get(channelUID);
|
||||
StateDescriptionFragment newStateDescriptionFragment = StateDescriptionFragmentBuilder.create()
|
||||
.withMinimum(BigDecimal.valueOf(minKelvin)).withMaximum(BigDecimal.valueOf(maxKelvin))
|
||||
.withStep(BigDecimal.valueOf(100)).withPattern("%.0f K").build();
|
||||
if (!newStateDescriptionFragment.equals(oldStateDescriptionFragment)) {
|
||||
stateDescriptionFragments.put(channelUID, newStateDescriptionFragment);
|
||||
ItemChannelLinkRegistry itemChannelLinkRegistry = this.itemChannelLinkRegistry;
|
||||
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
|
||||
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(),
|
||||
newStateDescriptionFragment, oldStateDescriptionFragment));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,11 +14,21 @@
|
||||
<label>MAC Address</label>
|
||||
<description>MAC Address of the device</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s">
|
||||
<parameter name="refreshInterval" type="integer" unit="s" min="2" max="60" step="1">
|
||||
<label>Light Refresh Interval</label>
|
||||
<description>The amount of time that passes until the device is refreshed (in seconds)</description>
|
||||
<default>2</default>
|
||||
</parameter>
|
||||
<parameter name="minKelvin" type="integer" unit="K" required="false" min="2000" max="9000" step="100">
|
||||
<label>Minimum Color Temperature</label>
|
||||
<description>The minimum color temperature that the light supports (in Kelvin)</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="maxKelvin" type="integer" unit="K" required="false" min="2000" max="9000" step="100">
|
||||
<label>Maximum Color Temperature</label>
|
||||
<description>The maximum color temperature that the light supports (in Kelvin)</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
|
@ -1,5 +1,28 @@
|
||||
# add-on
|
||||
|
||||
addon.govee.name = Govee Lan-API Binding
|
||||
addon.govee.description = This is the binding for handling Govee Lights via the LAN-API interface.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.govee.govee-light.label = Govee Light
|
||||
thing-type.govee.govee-light.description = Govee light controllable via LAN API
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.govee.govee-light.hostname.label = Hostname/IP Address
|
||||
thing-type.config.govee.govee-light.hostname.description = Hostname or IP address of the device
|
||||
thing-type.config.govee.govee-light.macAddress.label = MAC Address
|
||||
thing-type.config.govee.govee-light.macAddress.description = MAC Address of the device
|
||||
thing-type.config.govee.govee-light.maxKelvin.label = Maximum Color Temperature
|
||||
thing-type.config.govee.govee-light.maxKelvin.description = The maximum color temperature that the light supports (in Kelvin)
|
||||
thing-type.config.govee.govee-light.minKelvin.label = Minimum Color Temperature
|
||||
thing-type.config.govee.govee-light.minKelvin.description = The minimum color temperature that the light supports (in Kelvin)
|
||||
thing-type.config.govee.govee-light.refreshInterval.label = Light Refresh Interval
|
||||
thing-type.config.govee.govee-light.refreshInterval.description = The amount of time that passes until the device is refreshed (in seconds)
|
||||
|
||||
# add-on
|
||||
|
||||
addon.name = Govee Binding
|
||||
addon.description = This is the binding for handling Govee Lights via the LAN-API interface.
|
||||
|
||||
@ -15,68 +38,127 @@ thing-type.config.govee-light.refreshInterval.description = The amount of time t
|
||||
|
||||
# product names
|
||||
|
||||
discovery.govee-light.H619Z = H619Z RGBIC Pro LED Strip Lights
|
||||
discovery.govee-light.H6042 = H6042 Govee TV Light Bar #2
|
||||
discovery.govee-light.H6043 = H6043 Govee TV Light Bars #2
|
||||
discovery.govee-light.H6046 = H6046 RGBIC TV Light Bars
|
||||
discovery.govee-light.H6047 = H6047 RGBIC Gaming Light Bars with Smart Controller
|
||||
discovery.govee-light.H6051 = H6051 Aura - Smart Table Lamp
|
||||
discovery.govee-light.H6052 = H6052 Govee Table Lamp
|
||||
discovery.govee-light.H6056 = H6056 H6056 Flow Plus
|
||||
discovery.govee-light.H6059 = H6059 RGBWW Night Light for Kids
|
||||
discovery.govee-light.H6061 = H6061 Glide Hexa LED Panels
|
||||
discovery.govee-light.H6062 = H6062 Glide Wall Light
|
||||
discovery.govee-light.H6063 = H6063 Gaming 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.H606A = H606A Glide Hexa Light Panel Ultra
|
||||
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.H6076 = H6076 RGBICW Smart Corner Floor Lamp
|
||||
discovery.govee-light.H6078 = H6078 Cylinder Floor Lamp
|
||||
discovery.govee-light.H607C = H607C Floor Lamp #2
|
||||
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.H61D3 = H61D3 Neon Rope Light 2 3m
|
||||
discovery.govee-light.H61D5 = H61D5 Neon Rope Light 2 5m
|
||||
discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10
|
||||
discovery.govee-light.H61A8 = H61A8 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.H6088 = H6088 RGBIC Cube Wall Sconces
|
||||
discovery.govee-light.H608A = H608A String Downlights 5M
|
||||
discovery.govee-light.H608B = H608B String Downlights 3M
|
||||
discovery.govee-light.H608C = H608C String Downlights 2M
|
||||
discovery.govee-light.H608D = H608D String Downlights 10M
|
||||
discovery.govee-light.H60A0 = H60A0 Ceiling Light
|
||||
discovery.govee-light.H60A1 = H60A1 Smart Ceiling Light
|
||||
discovery.govee-light.H610A = H610A Glide Lively Wall Lights
|
||||
discovery.govee-light.H610B = H610B Music Wall Lights
|
||||
discovery.govee-light.H6110 = H6110 2x5M Multicolor with Alexa
|
||||
discovery.govee-light.H6117 = H6117 Dream Color LED Strip Light 10M
|
||||
discovery.govee-light.H6141 = H6141 5M Smart Multicolor Strip Light
|
||||
discovery.govee-light.H6143 = H6143 5M Strip Light
|
||||
discovery.govee-light.H6144 = H6144 2x5M Strip Light
|
||||
discovery.govee-light.H6159 = H6159 RGB Light Strip
|
||||
discovery.govee-light.H615A = H615A 5M Light Strip with Alexa
|
||||
discovery.govee-light.H615B = H615B 10M Light Strip with Alexa
|
||||
discovery.govee-light.H615C = H615C 15M Light Strip with Alexa
|
||||
discovery.govee-light.H615D = H615D 20M Light Strip with Alexa
|
||||
discovery.govee-light.H615E = H615E 30M Light Strip with Alexa
|
||||
discovery.govee-light.H6163 = H6163 Dreamcolor LED Strip Light 5M
|
||||
discovery.govee-light.H6167 = H6167 TV Backlight 2.4M
|
||||
discovery.govee-light.H6168 = H6168 TV Backlight 2x0.7M+2x1.2M
|
||||
discovery.govee-light.H616C = H616C Outdoor Strip 10M
|
||||
discovery.govee-light.H616D = H616D Outdoor Strip 2x7.5M
|
||||
discovery.govee-light.H616E = H616E Outdoor Strip 2x10M
|
||||
discovery.govee-light.H6172 = H6172 Outdoor LED Strip 10m
|
||||
discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight
|
||||
discovery.govee-light.H6173 = H6173 RGBIC Outdoor Strip Lights
|
||||
discovery.govee-light.H6175 = H6175 RGBIC Outdoor Strip Lights 10M
|
||||
discovery.govee-light.H6176 = H6176 RGBIC Outdoor Strip Lights 30M
|
||||
discovery.govee-light.H6182 = H6182 WiFi Multicolor TV Strip Light
|
||||
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.H618E = H618E LED Strip Lights 22m
|
||||
discovery.govee-light.H618F = H618F RGBIC LED Strip Lights
|
||||
discovery.govee-light.H619A = H619A Strip Lights With Protective Coating 5M
|
||||
discovery.govee-light.H619B = H619B Strip Lights With Protective Coating 7.5M
|
||||
discovery.govee-light.H619C = H619C Strip Lights With Protective Coating with Alexa 10M
|
||||
discovery.govee-light.H619D = H619D PRO LED Strip Lights with Alexa 2x7.5M
|
||||
discovery.govee-light.H619E = H619E Strip Lights With Protective Coating with Alexa 2x10M
|
||||
discovery.govee-light.H619Z = H619Z Pro LED Strip Lights 3M
|
||||
discovery.govee-light.H61A0 = H61A0 RGBIC Neon Rope Light 3M
|
||||
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 4M
|
||||
discovery.govee-light.H61A5 = H61A5 Neon LED Strip Light 10M
|
||||
discovery.govee-light.H61A8 = H61A8 Neon Rope Light 10M
|
||||
discovery.govee-light.H61A9 = H61A8 Neon Rope Light 20M
|
||||
discovery.govee-light.H61B1 = H61B1 Strip Light with Cover 5M
|
||||
discovery.govee-light.H61B2 = H61B2 RGBIC Neon TV Backlight 3M
|
||||
discovery.govee-light.H61BA = H61BA LED Strip Light 5M
|
||||
discovery.govee-light.H61BC = H61BC LED Strip Light 10M
|
||||
discovery.govee-light.H61BE = H61BE LED Strip Light 2x10M
|
||||
discovery.govee-light.H61C2 = H61C2 Neon LED Strip Light 2M
|
||||
discovery.govee-light.H61C3 = H61C2 Neon LED Strip Light 3M
|
||||
discovery.govee-light.H61C5 = H61C2 Neon LED Strip Light 5M
|
||||
discovery.govee-light.H61D3 = H61D3 Neon Rope Light 2 3m
|
||||
discovery.govee-light.H61D5 = H61D5 Neon Rope Light 2 5m
|
||||
discovery.govee-light.H61E0 = H61E0 LED Strip Light M1
|
||||
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.H7033 = H7033 LED-Bulb String Lights
|
||||
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.H7052 = H7052 Outdoor Ground Lights 15M
|
||||
discovery.govee-light.H7053 = H7052 Outdoor Ground Lights 30M
|
||||
discovery.govee-light.H7055 = H7055 Pathway Light
|
||||
discovery.govee-light.H705A = H705A Permanent Outdoor Lights 30M
|
||||
discovery.govee-light.H705B = H705B Permanent Outdoor Lights 15M
|
||||
discovery.govee-light.H705C = H705C Permanent Outdoor Lights 45M
|
||||
discovery.govee-light.H705D = H705D Permanent Outdoor Lights #2 15M
|
||||
discovery.govee-light.H705E = H705E Permanent Outdoor Lights #2 30M
|
||||
discovery.govee-light.H705F = H705F Permanent Outdoor Lights #2 45M
|
||||
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.H7063 = H7063 Outdoor Flood Lights
|
||||
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
|
||||
discovery.govee-light.H7066 = H7066 Outdoor Spot Lights
|
||||
discovery.govee-light.H706A = H706A Permanent Outdoor Lights Pro 30M
|
||||
discovery.govee-light.H706B = H706B Permanent Outdoor Lights Pro 45M
|
||||
discovery.govee-light.H706C = H706C Permanent Outdoor Lights Pro 60M
|
||||
discovery.govee-light.H7070 = H7070 Outdoor Projector Light
|
||||
discovery.govee-light.H7075 = H7075 Outdoor Wall Light
|
||||
discovery.govee-light.H70B1 = H70B1 520 LED Curtain Lights
|
||||
discovery.govee-light.H70BC = H70BC 400 LED Curtain Lights
|
||||
discovery.govee-light.H70C1 = H70C1 RGBIC String Light 10M
|
||||
discovery.govee-light.H70C2 = H70C2 RGBIC String Light 20M
|
||||
discovery.govee-light.H805A = H805A Permanent Outdoor Lights Elite 30M
|
||||
discovery.govee-light.H805B = H805B Permanent Outdoor Lights Elite 15M
|
||||
discovery.govee-light.H805C = H805C Permanent Outdoor Lights Elite 45M
|
||||
|
||||
# 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
|
||||
offline.configuration-error.invalid-color-temperature-range = Invalid color temperature range
|
||||
|
@ -11,22 +11,14 @@
|
||||
<channels>
|
||||
<channel id="color" typeId="system.color"/>
|
||||
<channel id="color-temperature" typeId="system.color-temperature"/>
|
||||
<channel id="color-temperature-abs" typeId="color-temperature-abs"/>
|
||||
<channel id="color-temperature-abs" typeId="system.color-temperature-abs"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
</properties>
|
||||
|
||||
<config-description-ref uri="thing-type:govee:govee-light"/>
|
||||
</thing-type>
|
||||
|
||||
|
||||
<channel-type id="color-temperature-abs" advanced="true">
|
||||
<item-type unitHint="K">Number:Temperature</item-type>
|
||||
<label>Color Temperature</label>
|
||||
<description>Controls the color temperature of the light in Kelvin</description>
|
||||
<category>ColorLight</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,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="govee:govee-light">
|
||||
<instruction-set targetVersion="1">
|
||||
<update-channel id="color-temperature-abs">
|
||||
<type>system:color-temperature-abs</type>
|
||||
</update-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
@ -12,8 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.govee.internal;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
@ -26,14 +25,15 @@ import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* The {@link GoveeHandlerMock} is responsible for mocking {@link GoveeHandler}
|
||||
*
|
||||
*
|
||||
* @author Leo Siepel - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeHandlerMock extends GoveeHandler {
|
||||
|
||||
public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager) {
|
||||
super(thing, communicationManager);
|
||||
public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager,
|
||||
GoveeStateDescriptionProvider stateDescriptionProvider) {
|
||||
super(thing, communicationManager, stateDescriptionProvider);
|
||||
|
||||
executorService = Mockito.mock(ScheduledExecutorService.class);
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
|
@ -12,12 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.govee.internal;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@ -87,7 +83,10 @@ public class GoveeSerializeGoveeHandlerTest {
|
||||
|
||||
private static GoveeHandlerMock createAndInitHandler(final ThingHandlerCallback callback, final Thing thing) {
|
||||
CommunicationManager communicationManager = mock(CommunicationManager.class);
|
||||
final GoveeHandlerMock handler = spy(new GoveeHandlerMock(thing, communicationManager));
|
||||
GoveeStateDescriptionProvider stateDescriptionProvider = mock(GoveeStateDescriptionProvider.class);
|
||||
|
||||
final GoveeHandlerMock handler = spy(
|
||||
new GoveeHandlerMock(thing, communicationManager, stateDescriptionProvider));
|
||||
|
||||
handler.setCallback(callback);
|
||||
handler.initialize();
|
||||
@ -135,7 +134,7 @@ public class GoveeSerializeGoveeHandlerTest {
|
||||
|
||||
verify(callback).stateUpdated(
|
||||
new ChannelUID(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS),
|
||||
getState(2000, Units.KELVIN));
|
||||
getState(9000, Units.KELVIN));
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user