From 5c762848b5adc8a973e91a345c2ca21b7adedb43 Mon Sep 17 00:00:00 2001 From: JankKeks <37385210+JankKeks@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:21:53 +0100 Subject: [PATCH 1/3] [freeathome] Fix not updating values of room temperature devices (#17957) * Fixed Pathnaming matching new binding name Signed-off-by: JankKeks --- .../internal/FreeAtHomeBindingConstants.java | 0 .../internal/FreeAtHomeDiscoveryService.java | 0 .../internal/FreeAtHomeHandlerFactory.java | 0 .../FreeAtHomeBridgeHandlerConfiguration.java | 0 .../FreeAtHomeDeviceHandlerConfiguration.java | 0 .../internal/datamodel/FreeAtHomeDatapoint.java | 0 .../internal/datamodel/FreeAtHomeDatapointGroup.java | 0 .../internal/datamodel/FreeAtHomeDeviceChannel.java | 0 .../datamodel/FreeAtHomeDeviceDescription.java | 0 .../internal/handler/FreeAtHomeBridgeHandler.java | 5 +++-- .../internal/handler/FreeAtHomeDeviceHandler.java | 11 +++++++---- .../handler/FreeAtHomeDeviceStateListener.java | 0 .../internal/type/FreeAtHomeChannelTypeProvider.java | 0 .../type/FreeAtHomeChannelTypeProviderImpl.java | 0 .../internal/type/FreeAtHomeThingTypeProvider.java | 0 .../type/FreeAtHomeThingTypeProviderImpl.java | 0 .../internal/util/FidTranslationUtils.java | 0 .../internal/util/FreeAtHomeGeneralException.java | 0 .../util/FreeAtHomeHttpCommunicationException.java | 0 .../internal/util/PIdContainerClass.java | 0 .../internal/util/PidTranslationUtils.java | 0 .../internal/util/UidUtils.java | 0 .../BinaryValueStateConverter.java | 0 .../BooleanValueStateConverter.java | 0 .../DecimalValueStateConverter.java | 0 .../ShuttercontrolValueStateConverter.java | 0 .../valuestateconverter/ValueStateConverter.java | 0 27 files changed, 10 insertions(+), 6 deletions(-) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/FreeAtHomeBindingConstants.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/FreeAtHomeDiscoveryService.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/FreeAtHomeHandlerFactory.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/configuration/FreeAtHomeBridgeHandlerConfiguration.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/configuration/FreeAtHomeDeviceHandlerConfiguration.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/datamodel/FreeAtHomeDatapoint.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/datamodel/FreeAtHomeDatapointGroup.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/datamodel/FreeAtHomeDeviceChannel.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/datamodel/FreeAtHomeDeviceDescription.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/handler/FreeAtHomeBridgeHandler.java (99%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/handler/FreeAtHomeDeviceHandler.java (98%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/handler/FreeAtHomeDeviceStateListener.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/type/FreeAtHomeChannelTypeProvider.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/type/FreeAtHomeChannelTypeProviderImpl.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/type/FreeAtHomeThingTypeProvider.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/type/FreeAtHomeThingTypeProviderImpl.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/FidTranslationUtils.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/FreeAtHomeGeneralException.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/FreeAtHomeHttpCommunicationException.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/PIdContainerClass.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/PidTranslationUtils.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/util/UidUtils.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/valuestateconverter/BinaryValueStateConverter.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/valuestateconverter/BooleanValueStateConverter.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/valuestateconverter/DecimalValueStateConverter.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/valuestateconverter/ShuttercontrolValueStateConverter.java (100%) rename bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/{freeathomesystem => freeathome}/internal/valuestateconverter/ValueStateConverter.java (100%) diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeBindingConstants.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeBindingConstants.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeBindingConstants.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeBindingConstants.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeDiscoveryService.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeDiscoveryService.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeDiscoveryService.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeDiscoveryService.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeHandlerFactory.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeHandlerFactory.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/FreeAtHomeHandlerFactory.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/FreeAtHomeHandlerFactory.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/configuration/FreeAtHomeBridgeHandlerConfiguration.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/configuration/FreeAtHomeBridgeHandlerConfiguration.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/configuration/FreeAtHomeBridgeHandlerConfiguration.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/configuration/FreeAtHomeBridgeHandlerConfiguration.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/configuration/FreeAtHomeDeviceHandlerConfiguration.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/configuration/FreeAtHomeDeviceHandlerConfiguration.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/configuration/FreeAtHomeDeviceHandlerConfiguration.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/configuration/FreeAtHomeDeviceHandlerConfiguration.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDatapoint.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDatapoint.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDatapoint.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDatapoint.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDatapointGroup.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDatapointGroup.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDatapointGroup.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDatapointGroup.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDeviceChannel.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDeviceChannel.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDeviceChannel.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDeviceChannel.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDeviceDescription.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDeviceDescription.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/datamodel/FreeAtHomeDeviceDescription.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/datamodel/FreeAtHomeDeviceDescription.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeBridgeHandler.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeBridgeHandler.java similarity index 99% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeBridgeHandler.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeBridgeHandler.java index cf719f0e710..e7f82d06e04 100644 --- a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeBridgeHandler.java +++ b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeBridgeHandler.java @@ -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); } } } diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeDeviceHandler.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeDeviceHandler.java similarity index 98% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeDeviceHandler.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeDeviceHandler.java index 2165ae3cb2d..35a7d21e920 100644 --- a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeDeviceHandler.java +++ b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeDeviceHandler.java @@ -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(); diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeDeviceStateListener.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeDeviceStateListener.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/handler/FreeAtHomeDeviceStateListener.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/handler/FreeAtHomeDeviceStateListener.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeChannelTypeProvider.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeChannelTypeProvider.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeChannelTypeProvider.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeChannelTypeProvider.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeChannelTypeProviderImpl.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeChannelTypeProviderImpl.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeChannelTypeProviderImpl.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeChannelTypeProviderImpl.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeThingTypeProvider.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeThingTypeProvider.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeThingTypeProvider.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeThingTypeProvider.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeThingTypeProviderImpl.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeThingTypeProviderImpl.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/type/FreeAtHomeThingTypeProviderImpl.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/type/FreeAtHomeThingTypeProviderImpl.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FidTranslationUtils.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FidTranslationUtils.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FidTranslationUtils.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FidTranslationUtils.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FreeAtHomeGeneralException.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FreeAtHomeGeneralException.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FreeAtHomeGeneralException.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FreeAtHomeGeneralException.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FreeAtHomeHttpCommunicationException.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FreeAtHomeHttpCommunicationException.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/FreeAtHomeHttpCommunicationException.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/FreeAtHomeHttpCommunicationException.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/PIdContainerClass.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/PIdContainerClass.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/PIdContainerClass.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/PIdContainerClass.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/PidTranslationUtils.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/PidTranslationUtils.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/PidTranslationUtils.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/PidTranslationUtils.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/UidUtils.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/UidUtils.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/util/UidUtils.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/util/UidUtils.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/BinaryValueStateConverter.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/BinaryValueStateConverter.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/BinaryValueStateConverter.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/BinaryValueStateConverter.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/BooleanValueStateConverter.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/BooleanValueStateConverter.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/BooleanValueStateConverter.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/BooleanValueStateConverter.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/DecimalValueStateConverter.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/DecimalValueStateConverter.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/DecimalValueStateConverter.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/DecimalValueStateConverter.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/ShuttercontrolValueStateConverter.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/ShuttercontrolValueStateConverter.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/ShuttercontrolValueStateConverter.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/ShuttercontrolValueStateConverter.java diff --git a/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/ValueStateConverter.java b/bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/ValueStateConverter.java similarity index 100% rename from bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathomesystem/internal/valuestateconverter/ValueStateConverter.java rename to bundles/org.openhab.binding.freeathome/src/main/java/org/openhab/binding/freeathome/internal/valuestateconverter/ValueStateConverter.java From 5eb47a042f7d1906ad3d09c4135d2d25bbea3a61 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Mon, 23 Dec 2024 13:31:00 +0000 Subject: [PATCH 2/3] [govee] Fix brightness vs. color synchronization (#17812) * [govee] Fix synchronization of brightness Signed-off-by: Andrew Fiddian-Green --- bundles/org.openhab.binding.govee/README.md | 145 ++++-- .../govee/internal/CommunicationManager.java | 413 ++++++++++-------- .../govee/internal/GoveeBindingConstants.java | 5 +- .../govee/internal/GoveeConfiguration.java | 4 + .../govee/internal/GoveeDiscoveryService.java | 79 ++-- .../binding/govee/internal/GoveeHandler.java | 373 +++++++++------- .../govee/internal/GoveeHandlerFactory.java | 9 +- .../GoveeStateDescriptionProvider.java | 83 ++++ .../main/resources/OH-INF/config/config.xml | 12 +- .../resources/OH-INF/i18n/govee.properties | 144 ++++-- .../resources/OH-INF/thing/thing-types.xml | 20 +- .../resources/OH-INF/update/instructions.xml | 14 + .../govee/internal/GoveeHandlerMock.java | 10 +- .../GoveeSerializeGoveeHandlerTest.java | 15 +- 14 files changed, 863 insertions(+), 463 deletions(-) create mode 100644 bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeStateDescriptionProvider.java create mode 100644 bundles/org.openhab.binding.govee/src/main/resources/OH-INF/update/instructions.xml diff --git a/bundles/org.openhab.binding.govee/README.md b/bundles/org.openhab.binding.govee/README.md index 459f17aa5f3..25d2c288c46 100644 --- a/bundles/org.openhab.binding.govee/README.md +++ b/bundles/org.openhab.binding.govee/README.md @@ -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 diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java index c8cdee364b4..abecfb6ab16 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java @@ -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 thingHandlers = new HashMap<>(); - @Nullable - private StatusReceiver receiverThread; + + // list of Thing handler listeners that will receive state notifications + private final Map 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 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. + * + *
  • In case of status notifications it forwards the message to the Thing handler listener.
  • + *
  • In case of discovery notifications it forwards the message to the discovery listener.
  • + *
  • If there is neither a Thing handler listener, nor a discovery listener, it logs an error.
  • + */ + 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); } } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java index 35ca13bb647..713ac70e9bd 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeBindingConstants.java @@ -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"; } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java index 23932e8440d..fe2d0d6a8d1 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeConfiguration.java @@ -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; } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java index ef5a62b69fd..3d22c007f03 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeDiscoveryService.java @@ -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 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; + } + } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java index 6c8f51b1ab0..7fd81c6953a 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java @@ -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> 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 thingRefreshSender triggers an update to the Govee device. - * The device sends it back to the common port and the response is - * then received by the common #refreshStatusReceiver + * This thing related job thingTaskSender 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))); } } diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java index 907c3d41426..a9b5ec5e9ba 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandlerFactory.java @@ -38,11 +38,14 @@ import org.osgi.service.component.annotations.Reference; public class GoveeHandlerFactory extends BaseThingHandlerFactory { private static final Set 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; diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeStateDescriptionProvider.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeStateDescriptionProvider.java new file mode 100644 index 00000000000..b35e3e8ef4e --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeStateDescriptionProvider.java @@ -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 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)); + } + } +} diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml index e153efa4d80..9ddf0f4c14d 100644 --- a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/config/config.xml @@ -14,11 +14,21 @@ MAC Address of the device - + The amount of time that passes until the device is refreshed (in seconds) 2 + + + The minimum color temperature that the light supports (in Kelvin) + true + + + + The maximum color temperature that the light supports (in Kelvin) + true + diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties index b0a015db28f..d9c7d3a4b29 100644 --- a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/i18n/govee.properties @@ -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 diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml index 1a49746b315..b96fbc8d57f 100644 --- a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/thing/thing-types.xml @@ -11,22 +11,14 @@ - + + + + 1 + + - - - Number:Temperature - - Controls the color temperature of the light in Kelvin - ColorLight - - Control - ColorTemperature - - - - diff --git a/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/update/instructions.xml new file mode 100644 index 00000000000..9317395b6a8 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,14 @@ + + + + + + + system:color-temperature-abs + + + + + diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java index 7f048867a11..f3374b2453e 100644 --- a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java @@ -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) -> { diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java index cb92a747806..46565a1a66a 100644 --- a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java @@ -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(); } From d253e2cd2e6d830c28baaa9a73a3fe53f6055ad0 Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Tue, 24 Dec 2024 00:48:46 +1000 Subject: [PATCH 3/3] [jrubyscripting] Remove Compilable implementation (#17960) due to a bug in JRuby https://github.com/jruby/jruby/issues/8346 Signed-off-by: Jimmy Tanagra --- .../internal/JRubyEngineWrapper.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyEngineWrapper.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyEngineWrapper.java index 07d13577c7f..628cb23b794 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyEngineWrapper.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyEngineWrapper.java @@ -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);