From 2b72bbb824d628e4cd6c300aabac0acbe3f18069 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 2 Jul 2024 17:35:38 +0100 Subject: [PATCH 01/20] [hue] add support for enabling automations Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/HueBindingConstants.java | 4 + .../internal/handler/Clip2BridgeHandler.java | 85 ++++++++++++++++++- .../main/resources/OH-INF/i18n/hue.properties | 5 ++ .../main/resources/OH-INF/thing/bridge.xml | 4 + .../main/resources/OH-INF/thing/channels.xml | 9 ++ 5 files changed, 105 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index 8ed1c5ab4213c..f67644bb7e180 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; /** * The {@link HueBindingConstants} class defines common constants, which are @@ -200,4 +201,7 @@ public class HueBindingConstants { Map.entry(CHANNEL_LAST_UPDATED, CHANNEL_2_LAST_UPDATED)); public static final String ALL_LIGHTS_KEY = "discovery.group.all-lights.label"; + + public static final String CHANNEL_GROUP_AUTOMATION = "automations"; + public static final ChannelTypeUID CHANNEL_TYPE_AUTOMATION = new ChannelTypeUID(BINDING_ID, "automation-enabled"); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 3fdeeab9feb50..7cfa8bb726048 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -27,6 +28,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -50,7 +52,11 @@ import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.TlsTrustManagerProvider; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingRegistry; @@ -62,8 +68,12 @@ import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.builder.BridgeBuilder; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceRegistration; @@ -93,12 +103,14 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME); private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE); private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE); + private static final ResourceReference BEHAVIOR = new ResourceReference().setType(ResourceType.BEHAVIOR_INSTANCE); /** * List of resource references that need to be mass down loaded. * NOTE: the SCENE resources must be mass down loaded first! */ - private static final List MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE); + private static final List MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE, + BEHAVIOR); private final Logger logger = LoggerFactory.getLogger(Clip2BridgeHandler.class); @@ -107,6 +119,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final Bundle bundle; private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; + private final Set behaviourIds = new HashSet<>();; private @Nullable Clip2Bridge clip2Bridge; private @Nullable ServiceRegistration trustManagerRegistration; @@ -421,7 +434,23 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (RefreshType.REFRESH.equals(command)) { return; } - logger.warn("Bridge thing '{}' has no channels, only REFRESH command supported.", thing.getUID()); + + if (CHANNEL_GROUP_AUTOMATION.equals(channelUID.getGroupId())) { + try { + Resource resource = new Resource(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getId()) + .setEnabled(command); + + Resources resources = getClip2Bridge().putResource(resource); + + if (resources.hasErrors()) { + logger.info("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, + String.join("; ", resources.getErrors())); + } + } catch (ApiException | AssetNotLoadedException e) { + logger.warn("handleCommand({}, {}) error {}", channelUID, command, e.getMessage()); + } catch (InterruptedException e) { + } + } } @Override @@ -527,6 +556,7 @@ public void onResourcesEvent(List resources) { } private void onResourcesEventTask(List resources) { + onResources(resources); int numberOfResources = resources.size(); logger.debug("onResourcesEventTask() resource count {}", numberOfResources); Setters.mergeLightResources(resources); @@ -742,6 +772,10 @@ private void updateThingsNow() { resourceList.addAll(bridge.getResources(SMART_SCENE).getResources()); break; + case BEHAVIOR_INSTANCE: + updateChannels(resourceList); + continue; // do not forward the child things + default: break; } @@ -775,4 +809,51 @@ private void updateThingsScheduled(int delayMilliSeconds) { scheduledUpdateTask = scheduler.schedule(() -> updateThingsNow(), delayMilliSeconds, TimeUnit.MILLISECONDS); } } + + /** + * Create the automation channels + */ + private void updateChannels(List resources) { + List behaviors = resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()) + .toList(); + + if (behaviors.size() != behaviourIds.size() + || behaviors.stream().anyMatch(behavior -> !behaviourIds.contains(behavior.getId()))) { + behaviourIds.clear(); + behaviourIds.addAll(behaviors.stream().map(behavior -> behavior.getId()).collect(Collectors.toSet())); + + List allChannels = thing.getChannels().stream() + .filter(channel -> !CHANNEL_TYPE_AUTOMATION.equals(channel.getChannelTypeUID())).toList(); + allChannels.addAll(behaviors.stream().map(bi -> createAutomationChannel(bi)).toList()); + + updateThing(editThing().withChannels(allChannels).build()); + } + } + + /** + * Create an automation channel from a behaviour instance resource + */ + private Channel createAutomationChannel(Resource behaviourInstance) { + return ChannelBuilder + .create(new ChannelUID(new ChannelGroupUID(thing.getUID(), CHANNEL_GROUP_AUTOMATION), + behaviourInstance.getId()), CoreItemFactory.SWITCH) + .withLabel(behaviourInstance.getName()).withType(CHANNEL_TYPE_AUTOMATION) + .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); + } + + /** + * Process event resources list and update the automation channels if any + */ + public void onResources(Collection resources) { + for (Resource resource : resources) { + if (resource.getType() == ResourceType.BEHAVIOR_INSTANCE) { + Channel channel = thing.getChannel(resource.getId()); + if (channel != null) { + Boolean enabled = resource.getEnabled(); + State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; + updateState(channel.getUID(), state); + } + } + } + } } diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index 48219ed5cca42..84648f7183544 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -130,6 +130,10 @@ thing-type.config.hue.room.resourceId.description = Unique Resource ID of the ro thing-type.config.hue.zone.resourceId.label = Resource ID thing-type.config.hue.zone.resourceId.description = Unique Resource ID of the zone in the Hue bridge +# channel group types + +channel-group-type.hue.automations.label = Automations + # channel types channel-type.hue.advanced-brightness.label = Dimming Only @@ -144,6 +148,7 @@ channel-type.hue.alert.description = The alert channel allows a temporary change channel-type.hue.alert.state.option.NONE = None channel-type.hue.alert.state.option.SELECT = Alert channel-type.hue.alert.state.option.LSELECT = Long Alert +channel-type.hue.automation-enabled.label = Enable channel-type.hue.button-last-event.label = Button Last Event channel-type.hue.button-last-event.description = Numeric code (e.g. 1003) representing the last push button event. channel-type.hue.dark.label = Dark diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml index d2ea0f4bee6ee..a41c9e73bc0e6 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml @@ -67,6 +67,10 @@ The Hue Bridge represents a Philips Hue Bridge supporting API v2. + + + + serialNumber diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml index b25510071b6ba..dd5a407c2ccfd 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml @@ -286,4 +286,13 @@ Siren + + Switch + + + + + + + From da137b075d86f324996454dc96d9dd563942b6df Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 2 Jul 2024 17:53:09 +0100 Subject: [PATCH 02/20] [hue] small cosmetics Signed-off-by: Andrew Fiddian-Green --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 7cfa8bb726048..09d3078cedb7e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -774,7 +774,7 @@ private void updateThingsNow() { case BEHAVIOR_INSTANCE: updateChannels(resourceList); - continue; // do not forward the child things + continue; // do not forward to child things default: break; @@ -824,7 +824,7 @@ private void updateChannels(List resources) { List allChannels = thing.getChannels().stream() .filter(channel -> !CHANNEL_TYPE_AUTOMATION.equals(channel.getChannelTypeUID())).toList(); - allChannels.addAll(behaviors.stream().map(bi -> createAutomationChannel(bi)).toList()); + allChannels.addAll(behaviors.stream().map(behavior -> createAutomationChannel(behavior)).toList()); updateThing(editThing().withChannels(allChannels).build()); } From 10a1a1a3e900cbbbaafa2702b62790b551bf9315 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 10:12:36 +0100 Subject: [PATCH 03/20] [hue] debug logging Signed-off-by: Andrew Fiddian-Green --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 09d3078cedb7e..659c1f8860387 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -447,7 +447,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { String.join("; ", resources.getErrors())); } } catch (ApiException | AssetNotLoadedException e) { - logger.warn("handleCommand({}, {}) error {}", channelUID, command, e.getMessage()); + logger.warn("handleCommand({}, {}) error {}", channelUID, command, e.getMessage(), + logger.isDebugEnabled() ? e : null); } catch (InterruptedException e) { } } From 373ea9eba248d1bb11ed7534dcf2e5c6da6f7af6 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 16:41:21 +0100 Subject: [PATCH 04/20] [hue] fix parsing of JSON 'state' elements Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/api/dto/clip2/Resource.java | 10 +- .../hue/internal/clip2/Clip2DtoTest.java | 10 + .../src/test/resources/behavior_instance.json | 368 +++++++++++++----- 3 files changed, 294 insertions(+), 94 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index 6cae9c1e5904b..ae9b54cfb45f1 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -55,6 +55,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.google.gson.annotations.SerializedName; /** @@ -107,7 +108,7 @@ public class Resource { private @Nullable Dynamics dynamics; private @Nullable @SerializedName("contact_report") ContactReport contactReport; private @Nullable @SerializedName("tamper_reports") List tamperReports; - private @Nullable String state; + private @Nullable JsonElement state; /** * Constructor @@ -661,13 +662,14 @@ public State getSceneState() { /** * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean - * Optional whose value depends on the value of that element, or an empty Optional if it is not. + * Optional whose value depends on the value of that element, or an empty Optional if it is not. Note that in some + * resource types the 'state' element is not a String primitive. * * @return true, false, or empty. */ public Optional getSmartSceneActive() { - if (ResourceType.SMART_SCENE == getType()) { - String state = this.state; + if (ResourceType.SMART_SCENE == getType() && (state instanceof JsonPrimitive statePrimitive)) { + String state = statePrimitive.getAsString(); if (Objects.nonNull(state)) { return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state)); } diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java index 229d3bb9afa37..fc7be62844895 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java @@ -905,4 +905,14 @@ void testTimedEffectSetter() { assertTrue(resultEffect instanceof TimedEffects); assertEquals(Duration.ofMillis(44), ((TimedEffects) resultEffect).getDuration()); } + + @Test + void testBehaviorInstance() { + String json = load(ResourceType.BEHAVIOR_INSTANCE.name().toLowerCase()); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(2, list.size()); + } } diff --git a/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json b/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json index 2c2c4f4aa5364..3da61f4dea4e8 100644 --- a/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json +++ b/bundles/org.openhab.binding.hue/src/test/resources/behavior_instance.json @@ -1,91 +1,279 @@ { - "errors": [], - "data": [ - { - "configuration": { - "what": [ - { - "group": { - "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", - "rtype": "room" - }, - "recall": { - "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", - "rtype": "scene" - } - }, - { - "group": { - "rid": "8b529073-36dd-409b-8006-80df304048ea", - "rtype": "room" - }, - "recall": { - "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", - "rtype": "scene" - } - } - ], - "when_constrained": { - "type": "nighttime" - }, - "where": [ - { - "group": { - "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", - "rtype": "room" - } - }, - { - "group": { - "rid": "8b529073-36dd-409b-8006-80df304048ea", - "rtype": "room" - } - } - ] - }, - "dependees": [ - { - "level": "critical", - "target": { - "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", - "rtype": "room" - }, - "type": "ResourceDependee" - }, - { - "level": "critical", - "target": { - "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", - "rtype": "scene" - }, - "type": "ResourceDependee" - }, - { - "level": "critical", - "target": { - "rid": "8b529073-36dd-409b-8006-80df304048ea", - "rtype": "room" - }, - "type": "ResourceDependee" - }, - { - "level": "critical", - "target": { - "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", - "rtype": "scene" - }, - "type": "ResourceDependee" - } - ], - "enabled": true, - "id": "8d0ffbee-e24e-4d3e-b91a-5adc9ef5d49c", - "last_error": "", - "metadata": { - "name": "Coming home" - }, - "script_id": "fd60fcd1-4809-4813-b510-4a18856a595c", - "status": "running", - "type": "behavior_instance" - } - ] -} \ No newline at end of file + "errors": [ + ], + "data": [ + { + "id": "042284f9-eeae-4f1e-9560-cc73750c7d28", + "type": "behavior_instance", + "script_id": "67d9395b-4403-42cc-b5f0-740b699d67c6", + "enabled": true, + "state": { + "model_id": "RWL021", + "source_type": "device" + }, + "configuration": { + "buttons": { + "6615f1f1-f3f1-4a05-b8f7-581097458e34": { + "on_repeat": { + "action": "dim_down" + } + }, + "91ba8839-2bac-4175-9f8c-ed192842d549": { + "on_long_press": { + "action": "do_nothing" + }, + "on_short_release": { + "time_based_extended": { + "slots": [ + { + "actions": [ + { + "action": { + "recall": { + "rid": "f021deb5-5104-4752-aab3-2849f84da690", + "rtype": "scene" + } + } + } + ], + "start_time": { + "hour": 7, + "minute": 0 + } + }, + { + "actions": [ + { + "action": { + "recall": { + "rid": "4ddd4f8b-428c-4089-a9a1-c27df5259b9a", + "rtype": "scene" + } + } + } + ], + "start_time": { + "hour": 20, + "minute": 0 + } + }, + { + "actions": [ + { + "action": { + "recall": { + "rid": "af0c88c4-9dae-4767-8475-a3cca906390d", + "rtype": "scene" + } + } + } + ], + "start_time": { + "hour": 23, + "minute": 0 + } + } + ], + "with_off": { + "enabled": false + } + } + } + }, + "b0d5a0af-31fd-4189-9150-c551ff9033d7": { + "on_long_press": { + "action": "do_nothing" + }, + "on_short_release": { + "action": "all_off" + } + }, + "f95addfc-2f7c-453f-924d-ba496e07e5f9": { + "on_repeat": { + "action": "dim_up" + } + } + }, + "device": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "model_id": "RWL021", + "where": [ + { + "group": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + } + } + ] + }, + "dependees": [ + { + "target": { + "rid": "e130feac-3a5c-452e-a97d-5bca470783b3", + "rtype": "device" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "bdc282b3-750d-45dd-b6c4-12a2927d8951", + "rtype": "zone" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "f021deb5-5104-4752-aab3-2849f84da690", + "rtype": "scene" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "4ddd4f8b-428c-4089-a9a1-c27df5259b9a", + "rtype": "scene" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "af0c88c4-9dae-4767-8475-a3cca906390d", + "rtype": "scene" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "91ba8839-2bac-4175-9f8c-ed192842d549", + "rtype": "button" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "f95addfc-2f7c-453f-924d-ba496e07e5f9", + "rtype": "button" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "6615f1f1-f3f1-4a05-b8f7-581097458e34", + "rtype": "button" + }, + "level": "critical", + "type": "ResourceDependee" + }, + { + "target": { + "rid": "b0d5a0af-31fd-4189-9150-c551ff9033d7", + "rtype": "button" + }, + "level": "critical", + "type": "ResourceDependee" + } + ], + "status": "running", + "last_error": "", + "metadata": { + "name": "Worktops Dimmer Pad Right" + }, + "migrated_from": "/resourcelinks/5338" + }, + { + "configuration": { + "what": [ + { + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "recall": { + "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", + "rtype": "scene" + } + }, + { + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "recall": { + "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", + "rtype": "scene" + } + } + ], + "when_constrained": { + "type": "nighttime" + }, + "where": [ + { + "group": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + } + }, + { + "group": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + } + } + ] + }, + "dependees": [ + { + "level": "critical", + "target": { + "rid": "b8d28681-eba1-4156-85e2-96c9c5179fba", + "rtype": "room" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "11ac9c82-d031-43a6-a8d5-b6efdee72fe6", + "rtype": "scene" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "8b529073-36dd-409b-8006-80df304048ea", + "rtype": "room" + }, + "type": "ResourceDependee" + }, + { + "level": "critical", + "target": { + "rid": "8b65c749-3ad8-435e-a7ed-94e3cc99e9d7", + "rtype": "scene" + }, + "type": "ResourceDependee" + } + ], + "enabled": true, + "id": "8d0ffbee-e24e-4d3e-b91a-5adc9ef5d49c", + "last_error": "", + "metadata": { + "name": "Coming home" + }, + "script_id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "status": "running", + "type": "behavior_instance" + } + ] +} From c3f62e4d3c912c018860edf3af02b1431d3f52d0 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 19:21:49 +0100 Subject: [PATCH 05/20] [hue] bug fixes and optimizations Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/HueBindingConstants.java | 4 +- .../internal/handler/Clip2BridgeHandler.java | 57 +++++++++---------- .../main/resources/OH-INF/i18n/hue.properties | 4 +- .../main/resources/OH-INF/thing/bridge.xml | 2 +- .../main/resources/OH-INF/thing/channels.xml | 4 +- 5 files changed, 34 insertions(+), 37 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index f67644bb7e180..8b338dbe37631 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -202,6 +202,6 @@ public class HueBindingConstants { public static final String ALL_LIGHTS_KEY = "discovery.group.all-lights.label"; - public static final String CHANNEL_GROUP_AUTOMATION = "automations"; - public static final ChannelTypeUID CHANNEL_TYPE_AUTOMATION = new ChannelTypeUID(BINDING_ID, "automation-enabled"); + public static final String CHANNEL_GROUP_AUTOMATION = "automation"; + public static final ChannelTypeUID CHANNEL_TYPE_AUTOMATION = new ChannelTypeUID(BINDING_ID, "automation-enable"); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 659c1f8860387..c80b728462e3a 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -29,6 +29,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -69,7 +70,6 @@ import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.builder.BridgeBuilder; import org.openhab.core.thing.binding.builder.ChannelBuilder; -import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -119,7 +119,8 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final Bundle bundle; private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; - private final Set behaviourIds = new HashSet<>();; + private final Set behaviorIds = new HashSet<>();; + private final ChannelGroupUID automationChannelGroupUID; private @Nullable Clip2Bridge clip2Bridge; private @Nullable ServiceRegistration trustManagerRegistration; @@ -142,6 +143,7 @@ public Clip2BridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory, Th this.bundle = FrameworkUtil.getBundle(getClass()); this.localeProvider = localeProvider; this.translationProvider = translationProvider; + this.automationChannelGroupUID = new ChannelGroupUID(thing.getUID(), CHANNEL_GROUP_AUTOMATION); } /** @@ -437,11 +439,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (CHANNEL_GROUP_AUTOMATION.equals(channelUID.getGroupId())) { try { - Resource resource = new Resource(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getId()) + Resource resource = new Resource(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getIdWithoutGroup()) .setEnabled(command); - Resources resources = getClip2Bridge().putResource(resource); - if (resources.hasErrors()) { logger.info("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, String.join("; ", resources.getErrors())); @@ -818,43 +818,40 @@ private void updateChannels(List resources) { List behaviors = resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()) .toList(); - if (behaviors.size() != behaviourIds.size() - || behaviors.stream().anyMatch(behavior -> !behaviourIds.contains(behavior.getId()))) { - behaviourIds.clear(); - behaviourIds.addAll(behaviors.stream().map(behavior -> behavior.getId()).collect(Collectors.toSet())); + if (behaviors.size() != behaviorIds.size() + || behaviors.stream().anyMatch(behavior -> !behaviorIds.contains(behavior.getId()))) { + behaviorIds.clear(); + behaviorIds.addAll(behaviors.stream().map(b -> b.getId()).collect(Collectors.toSet())); - List allChannels = thing.getChannels().stream() - .filter(channel -> !CHANNEL_TYPE_AUTOMATION.equals(channel.getChannelTypeUID())).toList(); - allChannels.addAll(behaviors.stream().map(behavior -> createAutomationChannel(behavior)).toList()); + Stream newChannels = behaviors.stream().map(b -> createAutomationChannel(b)); + Stream oldchannels = thing.getChannels().stream() + .filter(c -> !CHANNEL_TYPE_AUTOMATION.equals(c.getChannelTypeUID())); - updateThing(editThing().withChannels(allChannels).build()); + updateThing(editThing().withChannels(Stream.concat(oldchannels, newChannels).toList()).build()); + onResources(behaviors); + + logger.debug("Bridge created {} automation channels", behaviors.size()); } } /** - * Create an automation channel from a behaviour instance resource + * Create an automation channel from a behavior instance resource */ - private Channel createAutomationChannel(Resource behaviourInstance) { + private Channel createAutomationChannel(Resource behavior) { return ChannelBuilder - .create(new ChannelUID(new ChannelGroupUID(thing.getUID(), CHANNEL_GROUP_AUTOMATION), - behaviourInstance.getId()), CoreItemFactory.SWITCH) - .withLabel(behaviourInstance.getName()).withType(CHANNEL_TYPE_AUTOMATION) - .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); + .create(new ChannelUID(automationChannelGroupUID, behavior.getId()), CoreItemFactory.SWITCH) + .withLabel(behavior.getName()).withType(CHANNEL_TYPE_AUTOMATION).build(); } /** - * Process event resources list and update the automation channels if any + * Process event resources list and update the automation channels */ public void onResources(Collection resources) { - for (Resource resource : resources) { - if (resource.getType() == ResourceType.BEHAVIOR_INSTANCE) { - Channel channel = thing.getChannel(resource.getId()); - if (channel != null) { - Boolean enabled = resource.getEnabled(); - State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; - updateState(channel.getUID(), state); - } - } - } + resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()).forEach(r -> { + ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, r.getId()); + Boolean enabled = r.getEnabled(); + State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; + updateState(channelUID, state); + }); } } diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index 84648f7183544..9a9af16afd7cd 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -132,7 +132,7 @@ thing-type.config.hue.zone.resourceId.description = Unique Resource ID of the zo # channel group types -channel-group-type.hue.automations.label = Automations +channel-group-type.hue.automation.label = Automations # channel types @@ -148,7 +148,7 @@ channel-type.hue.alert.description = The alert channel allows a temporary change channel-type.hue.alert.state.option.NONE = None channel-type.hue.alert.state.option.SELECT = Alert channel-type.hue.alert.state.option.LSELECT = Long Alert -channel-type.hue.automation-enabled.label = Enable +channel-type.hue.automation-enable.label = Enable channel-type.hue.button-last-event.label = Button Last Event channel-type.hue.button-last-event.description = Numeric code (e.g. 1003) representing the last push button event. channel-type.hue.dark.label = Dark diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml index a41c9e73bc0e6..f763a8d23cd0a 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/bridge.xml @@ -68,7 +68,7 @@ The Hue Bridge represents a Philips Hue Bridge supporting API v2. - + serialNumber diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml index dd5a407c2ccfd..a3e382990424d 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml @@ -286,12 +286,12 @@ Siren - + Switch - + From b21a2e0f133bea8e5bd2b283b2234e11a5d23097 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 19:46:47 +0100 Subject: [PATCH 06/20] [hue] add isStateNull() filter Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/api/dto/clip2/Resource.java | 4 ++++ .../hue/internal/handler/Clip2BridgeHandler.java | 15 ++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index ae9b54cfb45f1..d37cb824004e7 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -929,4 +929,8 @@ public String toString() { return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35), getType().name().toLowerCase()); } + + public boolean isStateNull() { + return Objects.isNull(state); + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index c80b728462e3a..9710cfb614e48 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -816,7 +816,7 @@ private void updateThingsScheduled(int delayMilliSeconds) { */ private void updateChannels(List resources) { List behaviors = resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()) - .toList(); + .filter(r -> r.isStateNull()).toList(); if (behaviors.size() != behaviorIds.size() || behaviors.stream().anyMatch(behavior -> !behaviorIds.contains(behavior.getId()))) { @@ -847,11 +847,12 @@ private Channel createAutomationChannel(Resource behavior) { * Process event resources list and update the automation channels */ public void onResources(Collection resources) { - resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()).forEach(r -> { - ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, r.getId()); - Boolean enabled = r.getEnabled(); - State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; - updateState(channelUID, state); - }); + resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()).filter(r -> r.isStateNull()) + .forEach(r -> { + ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, r.getId()); + Boolean enabled = r.getEnabled(); + State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; + updateState(channelUID, state); + }); } } From 085d92903cb01d2645bc6fcd762d122370528eba Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 23:26:09 +0100 Subject: [PATCH 07/20] [hue] simplify filters Signed-off-by: Andrew Fiddian-Green --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 9710cfb614e48..d6f0f5b27d0bd 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -815,8 +815,8 @@ private void updateThingsScheduled(int delayMilliSeconds) { * Create the automation channels */ private void updateChannels(List resources) { - List behaviors = resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()) - .filter(r -> r.isStateNull()).toList(); + List behaviors = resources.stream() + .filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()).toList(); if (behaviors.size() != behaviorIds.size() || behaviors.stream().anyMatch(behavior -> !behaviorIds.contains(behavior.getId()))) { @@ -847,7 +847,7 @@ private Channel createAutomationChannel(Resource behavior) { * Process event resources list and update the automation channels */ public void onResources(Collection resources) { - resources.stream().filter(r -> ResourceType.BEHAVIOR_INSTANCE == r.getType()).filter(r -> r.isStateNull()) + resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()) .forEach(r -> { ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, r.getId()); Boolean enabled = r.getEnabled(); From 686123fca4faf4ae81ba22a3eb365a858a193858 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 23:42:51 +0100 Subject: [PATCH 08/20] [hue] centralize filter for automation resources Signed-off-by: Andrew Fiddian-Green --- .../internal/handler/Clip2BridgeHandler.java | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index d6f0f5b27d0bd..5fe9d1715819e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -119,7 +119,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final Bundle bundle; private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; - private final Set behaviorIds = new HashSet<>();; + private final Set automationIds = new HashSet<>();; private final ChannelGroupUID automationChannelGroupUID; private @Nullable Clip2Bridge clip2Bridge; @@ -815,22 +815,21 @@ private void updateThingsScheduled(int delayMilliSeconds) { * Create the automation channels */ private void updateChannels(List resources) { - List behaviors = resources.stream() - .filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()).toList(); + List automations = getAutomationsStream(resources).toList(); - if (behaviors.size() != behaviorIds.size() - || behaviors.stream().anyMatch(behavior -> !behaviorIds.contains(behavior.getId()))) { - behaviorIds.clear(); - behaviorIds.addAll(behaviors.stream().map(b -> b.getId()).collect(Collectors.toSet())); + if (automations.size() != automationIds.size() + || automations.stream().anyMatch(a -> !automationIds.contains(a.getId()))) { + automationIds.clear(); + automationIds.addAll(automations.stream().map(a -> a.getId()).collect(Collectors.toSet())); - Stream newChannels = behaviors.stream().map(b -> createAutomationChannel(b)); + Stream newChannels = automations.stream().map(a -> createAutomationChannel(a)); Stream oldchannels = thing.getChannels().stream() .filter(c -> !CHANNEL_TYPE_AUTOMATION.equals(c.getChannelTypeUID())); updateThing(editThing().withChannels(Stream.concat(oldchannels, newChannels).toList()).build()); - onResources(behaviors); + onResources(automations); - logger.debug("Bridge created {} automation channels", behaviors.size()); + logger.debug("Bridge created {} automation channels", automations.size()); } } @@ -847,12 +846,19 @@ private Channel createAutomationChannel(Resource behavior) { * Process event resources list and update the automation channels */ public void onResources(Collection resources) { - resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()) - .forEach(r -> { - ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, r.getId()); - Boolean enabled = r.getEnabled(); - State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; - updateState(channelUID, state); - }); + getAutomationsStream(resources).forEach(a -> { + ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, a.getId()); + Boolean enabled = a.getEnabled(); + State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; + updateState(channelUID, state); + }); + } + + /** + * Create a filtered resource stream that contains only automation resources + */ + private Stream getAutomationsStream(Collection resources) { + // TODO fine tune the filter to exclude tap dial switches + return resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()); } } From dd55bd6ce67e542c2cbb1c226b578d351fb3965f Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Wed, 3 Jul 2024 23:46:25 +0100 Subject: [PATCH 09/20] [hue] refactoring Signed-off-by: Andrew Fiddian-Green --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 5fe9d1715819e..aa57494115cc1 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -834,12 +834,12 @@ private void updateChannels(List resources) { } /** - * Create an automation channel from a behavior instance resource + * Create an automation channel from an automation resource */ - private Channel createAutomationChannel(Resource behavior) { + private Channel createAutomationChannel(Resource automation) { return ChannelBuilder - .create(new ChannelUID(automationChannelGroupUID, behavior.getId()), CoreItemFactory.SWITCH) - .withLabel(behavior.getName()).withType(CHANNEL_TYPE_AUTOMATION).build(); + .create(new ChannelUID(automationChannelGroupUID, automation.getId()), CoreItemFactory.SWITCH) + .withLabel(automation.getName()).withType(CHANNEL_TYPE_AUTOMATION).build(); } /** From f2ee3ac12e4c6f350dd16c24698f0bf9b3ab883a Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Thu, 4 Jul 2024 10:36:52 +0100 Subject: [PATCH 10/20] [hue] add i18n Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/handler/Clip2BridgeHandler.java | 15 +++++++++++++-- .../src/main/resources/OH-INF/i18n/hue.properties | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index aa57494115cc1..55ba1c39ff128 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -105,6 +105,9 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE); private static final ResourceReference BEHAVIOR = new ResourceReference().setType(ResourceType.BEHAVIOR_INSTANCE); + private static final String AUTOMATION_CHANNEL_LABEL_KEY = "dynamic-channel.automation-enable.label"; + private static final String AUTOMATION_CHANNEL_DESCRIPTION_KEY = "dynamic-channel.automation-enable.description"; + /** * List of resource references that need to be mass down loaded. * NOTE: the SCENE resources must be mass down loaded first! @@ -837,9 +840,18 @@ private void updateChannels(List resources) { * Create an automation channel from an automation resource */ private Channel createAutomationChannel(Resource automation) { + String label = Objects.requireNonNullElse(translationProvider.getText(bundle, AUTOMATION_CHANNEL_LABEL_KEY, + AUTOMATION_CHANNEL_LABEL_KEY, localeProvider.getLocale(), automation.getName()), + AUTOMATION_CHANNEL_LABEL_KEY); + + String description = Objects.requireNonNullElse( + translationProvider.getText(bundle, AUTOMATION_CHANNEL_DESCRIPTION_KEY, + AUTOMATION_CHANNEL_DESCRIPTION_KEY, localeProvider.getLocale(), automation.getName()), + AUTOMATION_CHANNEL_DESCRIPTION_KEY); + return ChannelBuilder .create(new ChannelUID(automationChannelGroupUID, automation.getId()), CoreItemFactory.SWITCH) - .withLabel(automation.getName()).withType(CHANNEL_TYPE_AUTOMATION).build(); + .withLabel(label).withDescription(description).withType(CHANNEL_TYPE_AUTOMATION).build(); } /** @@ -858,7 +870,6 @@ public void onResources(Collection resources) { * Create a filtered resource stream that contains only automation resources */ private Stream getAutomationsStream(Collection resources) { - // TODO fine tune the filter to exclude tap dial switches return resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()); } } diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index 9a9af16afd7cd..4e3c8336f1f45 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -297,3 +297,8 @@ dynamics.command.label = Target Command dynamics.command.description = The target command state for the light(s) to transition to. dynamics.duration.label = Duration dynamics.duration.description = The dynamic transition duration in ms. + +# dynamic channels + +dynamic-channel.automation-enable.label = Enable ''{0}'' +dynamic-channel.automation-enable.description = Enable the ''{0}'' automation From 975be7b994a280b21d64cae856ba3e49b9788608 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Thu, 4 Jul 2024 10:52:53 +0100 Subject: [PATCH 11/20] [hue] read me Signed-off-by: Andrew Fiddian-Green --- bundles/org.openhab.binding.hue/doc/readme_v2.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bundles/org.openhab.binding.hue/doc/readme_v2.md b/bundles/org.openhab.binding.hue/doc/readme_v2.md index d9b84392f0a3a..a04ade44a496f 100644 --- a/bundles/org.openhab.binding.hue/doc/readme_v2.md +++ b/bundles/org.openhab.binding.hue/doc/readme_v2.md @@ -55,6 +55,17 @@ See [console command](#console-command-for-finding-resourceids) The configuration of all things (as described above) is the same regardless of whether it is a device containing a light, a button, or (one or more) sensors, or whether it is a room or zone. +### Channels for Bridges + +Bridge things support the following channels: + +| Channel ID | Item Type | Description | +|-------------------------------------------------|--------------------|---------------------------------------------| +| automation#11111111-2222-3333-4444-555555555555 | Switch | Enable / disable the respective automation. | + +The Bridge dynamically creates `automation` channels corresponding to the automations in the Hue App; +the '11111111-2222-3333-4444-555555555555' is the unique id of the respective automation. + ### Channels for Devices Device things support some of the following channels: From 5027164e57366e3f43cffe54adebe3608f87a9e8 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Thu, 4 Jul 2024 11:41:11 +0100 Subject: [PATCH 12/20] [hue] support refresh command Signed-off-by: Andrew Fiddian-Green --- .../hue/internal/handler/Clip2BridgeHandler.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 55ba1c39ff128..1996286723d2e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -436,15 +436,17 @@ public Collection> getServices() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (RefreshType.REFRESH.equals(command)) { - return; - } - if (CHANNEL_GROUP_AUTOMATION.equals(channelUID.getGroupId())) { try { - Resource resource = new Resource(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getIdWithoutGroup()) - .setEnabled(command); - Resources resources = getClip2Bridge().putResource(resource); + Resources resources; + if (RefreshType.REFRESH.equals(command)) { + resources = getClip2Bridge().getResources(new ResourceReference() + .setType(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getIdWithoutGroup())); + onResources(resources.getResources()); + } else { + resources = getClip2Bridge().putResource(new Resource(ResourceType.BEHAVIOR_INSTANCE) + .setId(channelUID.getIdWithoutGroup()).setEnabled(command)); + } if (resources.hasErrors()) { logger.info("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, String.join("; ", resources.getErrors())); From 7bced516011adf5323b61c09262381461dbf2fc1 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sat, 6 Jul 2024 08:59:21 +0100 Subject: [PATCH 13/20] [hue] adopt reviewer suggestions Signed-off-by: Andrew Fiddian-Green --- .../org.openhab.binding.hue/doc/readme_v2.md | 2 +- .../internal/handler/Clip2BridgeHandler.java | 53 +++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.hue/doc/readme_v2.md b/bundles/org.openhab.binding.hue/doc/readme_v2.md index a04ade44a496f..ca6b2cacb0238 100644 --- a/bundles/org.openhab.binding.hue/doc/readme_v2.md +++ b/bundles/org.openhab.binding.hue/doc/readme_v2.md @@ -57,7 +57,7 @@ The configuration of all things (as described above) is the same regardless of w ### Channels for Bridges -Bridge things support the following channels: +Bridge Things support the following channels: | Channel ID | Item Type | Description | |-------------------------------------------------|--------------------|---------------------------------------------| diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 1996286723d2e..545462d3e2b6e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -112,8 +112,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { * List of resource references that need to be mass down loaded. * NOTE: the SCENE resources must be mass down loaded first! */ - private static final List MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE, - BEHAVIOR); + private static final List MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE); private final Logger logger = LoggerFactory.getLogger(Clip2BridgeHandler.class); @@ -129,6 +128,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private @Nullable ServiceRegistration trustManagerRegistration; private @Nullable Clip2ThingDiscoveryService discoveryService; + private @Nullable Future channelRefreshTask; private @Nullable Future checkConnectionTask; private @Nullable Future updateOnlineStateTask; private @Nullable ScheduledFuture scheduledUpdateTask; @@ -283,9 +283,11 @@ private void disposeAssets() { logger.debug("disposeAssets() {}", this); synchronized (this) { assetsLoaded = false; + cancelTask(channelRefreshTask, true); cancelTask(checkConnectionTask, true); cancelTask(updateOnlineStateTask, true); cancelTask(scheduledUpdateTask, true); + channelRefreshTask = null; checkConnectionTask = null; updateOnlineStateTask = null; scheduledUpdateTask = null; @@ -778,10 +780,6 @@ private void updateThingsNow() { resourceList.addAll(bridge.getResources(SMART_SCENE).getResources()); break; - case BEHAVIOR_INSTANCE: - updateChannels(resourceList); - continue; // do not forward to child things - default: break; } @@ -792,6 +790,8 @@ private void updateThingsNow() { } }); } + + updateChannels(); } catch (ApiException | AssetNotLoadedException e) { if (logger.isDebugEnabled()) { logger.debug("updateThingsNow() unexpected exception", e); @@ -819,8 +819,22 @@ private void updateThingsScheduled(int delayMilliSeconds) { /** * Create the automation channels */ - private void updateChannels(List resources) { - List automations = getAutomationsStream(resources).toList(); + private void updateChannels() { + List resources; + try { + resources = getClip2Bridge().getResources(BEHAVIOR).getResources(); + } catch (ApiException | AssetNotLoadedException e) { + if (logger.isDebugEnabled()) { + logger.debug("updateChannels() unexpected exception", e); + } else { + logger.warn("Unexpected exception '{}' while updating channels.", e.getMessage()); + } + return; + } catch (InterruptedException e) { + return; + } + + List automations = getAutomationsList(resources); if (automations.size() != automationIds.size() || automations.stream().anyMatch(a -> !automationIds.contains(a.getId()))) { @@ -859,19 +873,26 @@ private Channel createAutomationChannel(Resource automation) { /** * Process event resources list and update the automation channels */ - public void onResources(Collection resources) { - getAutomationsStream(resources).forEach(a -> { - ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, a.getId()); - Boolean enabled = a.getEnabled(); + public void onResources(List resources) { + boolean refreshAutomationChannels = false; + for (Resource automation : getAutomationsList(resources)) { + String automationId = automation.getId(); + refreshAutomationChannels |= !automationIds.contains(automationId); + ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, automationId); + Boolean enabled = automation.getEnabled(); State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; updateState(channelUID, state); - }); + } + if (refreshAutomationChannels) { + channelRefreshTask = scheduler.submit(() -> updateChannels()); + } } /** - * Create a filtered resource stream that contains only automation resources + * Create a filtered resource list that contains only automation resources */ - private Stream getAutomationsStream(Collection resources) { - return resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()); + private List getAutomationsList(Collection resources) { + return resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()) + .toList(); } } From b3f7c706a6c60c2771c87d460a1d080a1c9bb81b Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Sat, 6 Jul 2024 09:02:48 +0100 Subject: [PATCH 14/20] [hue] adopt reviewer suggestion Signed-off-by: Andrew Fiddian-Green --- .../src/main/resources/OH-INF/thing/channels.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml index a3e382990424d..68c79045c885a 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/channels.xml @@ -289,6 +289,7 @@ Switch + Switch From cdb63e4d53b2450effc4491788a8108c0d3b2480 Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Fri, 19 Jul 2024 11:40:37 +0100 Subject: [PATCH 15/20] adopt reviewer suggestion Signed-off-by: AndrewFG --- .../binding/hue/internal/handler/Clip2BridgeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 545462d3e2b6e..d5302572abf7e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -450,7 +450,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { .setId(channelUID.getIdWithoutGroup()).setEnabled(command)); } if (resources.hasErrors()) { - logger.info("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, + logger.warn("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, String.join("; ", resources.getErrors())); } } catch (ApiException | AssetNotLoadedException e) { From fb16a700241633c17d11f6b5f7e4499668f9d955 Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Thu, 15 Aug 2024 17:57:53 +0100 Subject: [PATCH 16/20] Fix updating channels if automation renamed Signed-off-by: AndrewFG --- .../hue/internal/handler/Clip2BridgeHandler.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index d5302572abf7e..1d26cafa5fc55 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -121,7 +120,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final Bundle bundle; private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; - private final Set automationIds = new HashSet<>();; + private final Map automationIds = new ConcurrentHashMap<>();; private final ChannelGroupUID automationChannelGroupUID; private @Nullable Clip2Bridge clip2Bridge; @@ -837,9 +836,11 @@ private void updateChannels() { List automations = getAutomationsList(resources); if (automations.size() != automationIds.size() - || automations.stream().anyMatch(a -> !automationIds.contains(a.getId()))) { + || automations.stream().anyMatch(a -> !automationIds.containsKey(a.getId())) + || automations.stream().anyMatch(a -> !a.getName().equals(automationIds.get(a.getId())))) { + automationIds.clear(); - automationIds.addAll(automations.stream().map(a -> a.getId()).collect(Collectors.toSet())); + automationIds.putAll(automations.stream().collect(Collectors.toMap(a -> a.getId(), a -> a.getName()))); Stream newChannels = automations.stream().map(a -> createAutomationChannel(a)); Stream oldchannels = thing.getChannels().stream() @@ -877,7 +878,7 @@ public void onResources(List resources) { boolean refreshAutomationChannels = false; for (Resource automation : getAutomationsList(resources)) { String automationId = automation.getId(); - refreshAutomationChannels |= !automationIds.contains(automationId); + refreshAutomationChannels |= !automationIds.containsKey(automationId); ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, automationId); Boolean enabled = automation.getEnabled(); State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; From 9c8111e9d73199a263c7cb811309ef92bf3c67b9 Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Fri, 16 Aug 2024 14:50:02 +0100 Subject: [PATCH 17/20] Fix automation resource filter Signed-off-by: AndrewFG --- .../internal/api/dto/clip2/Configuration.java | 34 +++++++++++++++++++ .../hue/internal/api/dto/clip2/Resource.java | 7 ++-- .../internal/handler/Clip2BridgeHandler.java | 3 +- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java new file mode 100644 index 0000000000000..845644c1ed304 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java @@ -0,0 +1,34 @@ +/** + * 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.hue.internal.api.dto.clip2; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonElement; + +/** + * DTO for configuration of behavior_instance resources. + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public class Configuration { + private @Nullable JsonElement device; + + public boolean hasDeviceElement() { + return Objects.nonNull(device); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index d37cb824004e7..a07d59283e148 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -109,6 +109,7 @@ public class Resource { private @Nullable @SerializedName("contact_report") ContactReport contactReport; private @Nullable @SerializedName("tamper_reports") List tamperReports; private @Nullable JsonElement state; + private @Nullable Configuration configuration; /** * Constructor @@ -930,7 +931,9 @@ public String toString() { getType().name().toLowerCase()); } - public boolean isStateNull() { - return Objects.isNull(state); + public boolean isAutomationResource() { + Configuration configuration = this.configuration; + return Objects.nonNull(configuration) && !configuration.hasDeviceElement() + && (ResourceType.BEHAVIOR_INSTANCE == getType()); } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 1d26cafa5fc55..dd073cbcc9968 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -893,7 +893,6 @@ public void onResources(List resources) { * Create a filtered resource list that contains only automation resources */ private List getAutomationsList(Collection resources) { - return resources.stream().filter(r -> (ResourceType.BEHAVIOR_INSTANCE == r.getType()) && r.isStateNull()) - .toList(); + return resources.stream().filter(r -> r.isAutomationResource()).toList(); } } From 9ccf3b63d029342142a958844e64f6579083ace3 Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Sat, 17 Aug 2024 15:06:18 +0100 Subject: [PATCH 18/20] Filter automations by script-id category Signed-off-by: AndrewFG --- .../hue/internal/api/dto/clip2/MetaData.java | 6 +++ .../hue/internal/api/dto/clip2/Resource.java | 24 +++++++--- .../CategoryType.java} | 27 +++++++---- .../internal/handler/Clip2BridgeHandler.java | 46 ++++++++++++------- 4 files changed, 69 insertions(+), 34 deletions(-) rename bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/{Configuration.java => enums/CategoryType.java} (54%) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/MetaData.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/MetaData.java index 7194a582229c2..3a44de9e5ee93 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/MetaData.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/MetaData.java @@ -15,6 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hue.internal.api.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType; import com.google.gson.annotations.SerializedName; @@ -28,6 +29,7 @@ public class MetaData { private @Nullable String archetype; private @Nullable String name; private @Nullable @SerializedName("control_id") Integer controlId; + private @Nullable String category; public Archetype getArchetype() { return Archetype.of(archetype); @@ -37,6 +39,10 @@ public Archetype getArchetype() { return name; } + public CategoryType getCategory() { + return CategoryType.of(category); + } + public int getControlId() { Integer controlId = this.controlId; return controlId != null ? controlId.intValue() : 0; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index a07d59283e148..ddf86d2e2c8ad 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -28,6 +28,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ActionType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ButtonEventType; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContactStateType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType; @@ -109,7 +110,7 @@ public class Resource { private @Nullable @SerializedName("contact_report") ContactReport contactReport; private @Nullable @SerializedName("tamper_reports") List tamperReports; private @Nullable JsonElement state; - private @Nullable Configuration configuration; + private @Nullable @SerializedName("script_id") String scriptId; /** * Constructor @@ -345,6 +346,14 @@ public State getColorTemperaturePercentState() { return color; } + /** + * Return the resource's metadata category. + */ + public CategoryType getCategory() { + MetaData metaData = getMetaData(); + return Objects.nonNull(metaData) ? metaData.getCategory() : CategoryType.NULL; + } + /** * Return an HSB where the HS part is derived from the color xy JSON element (only), so the B part is 100% * @@ -650,6 +659,13 @@ public Optional getSceneActive() { return Optional.empty(); } + /** + * Return the scriptId if any. + */ + public @Nullable String getScriptId() { + return scriptId; + } + /** * If the getSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result is * present and 'true' (i.e. the scene is active) return the scene name. Or finally (the optional result is present @@ -930,10 +946,4 @@ public String toString() { return String.format("id:%s, type:%s", Objects.nonNull(id) ? id : "?" + " ".repeat(35), getType().name().toLowerCase()); } - - public boolean isAutomationResource() { - Configuration configuration = this.configuration; - return Objects.nonNull(configuration) && !configuration.hasDeviceElement() - && (ResourceType.BEHAVIOR_INSTANCE == getType()); - } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/CategoryType.java similarity index 54% rename from bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java rename to bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/CategoryType.java index 845644c1ed304..6b27bf6dd4c22 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Configuration.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/CategoryType.java @@ -10,25 +10,32 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hue.internal.api.dto.clip2; - -import java.util.Objects; +package org.openhab.binding.hue.internal.api.dto.clip2.enums; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import com.google.gson.JsonElement; - /** - * DTO for configuration of behavior_instance resources. + * Enum for 'category' fields. * * @author Andrew Fiddian-Green - Initial contribution */ @NonNullByDefault -public class Configuration { - private @Nullable JsonElement device; +public enum CategoryType { + ACCESSORY, + AUTOMATION, + ENTERTAINMENT, + NULL, + UNDEF; - public boolean hasDeviceElement() { - return Objects.nonNull(device); + public static CategoryType of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + return UNDEF; + } + } + return NULL; } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index dd073cbcc9968..5b5b55ef9fcea 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -38,6 +39,7 @@ import org.openhab.binding.hue.internal.api.dto.clip2.ResourceReference; import org.openhab.binding.hue.internal.api.dto.clip2.Resources; import org.openhab.binding.hue.internal.api.dto.clip2.enums.Archetype; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType; import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters; import org.openhab.binding.hue.internal.config.Clip2BridgeConfig; @@ -102,6 +104,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME); private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE); private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE); + private static final ResourceReference SCRIPT = new ResourceReference().setType(ResourceType.BEHAVIOR_SCRIPT); private static final ResourceReference BEHAVIOR = new ResourceReference().setType(ResourceType.BEHAVIOR_INSTANCE); private static final String AUTOMATION_CHANNEL_LABEL_KEY = "dynamic-channel.automation-enable.label"; @@ -121,6 +124,7 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; private final Map automationIds = new ConcurrentHashMap<>();; + private final Set automationScriptIds = new HashSet<>(); private final ChannelGroupUID automationChannelGroupUID; private @Nullable Clip2Bridge clip2Bridge; @@ -635,6 +639,7 @@ private void updateOnlineState() { logger.debug("updateOnlineState()"); connectRetriesRemaining = RECONNECT_MAX_TRIES; updateStatus(ThingStatus.ONLINE); + loadAutomationScriptIds(); updateThingsScheduled(500); Clip2ThingDiscoveryService discoveryService = this.discoveryService; if (Objects.nonNull(discoveryService)) { @@ -815,26 +820,38 @@ private void updateThingsScheduled(int delayMilliSeconds) { } } + /** + * Load the set of automation script ids. + */ + private void loadAutomationScriptIds() { + try { + automationScriptIds.clear(); + automationScriptIds.addAll(getClip2Bridge().getResources(SCRIPT).getResources().stream() + .filter(r -> CategoryType.AUTOMATION == r.getCategory()).map(r -> r.getId()) + .collect(Collectors.toSet())); + } catch (ApiException | AssetNotLoadedException e) { + logger.warn("loadAutomationScriptIds() unexpected exception {}", e.getMessage(), + logger.isDebugEnabled() ? e : null); + } catch (InterruptedException e) { + } + } + /** * Create the automation channels */ private void updateChannels() { - List resources; + List automations; try { - resources = getClip2Bridge().getResources(BEHAVIOR).getResources(); + automations = getClip2Bridge().getResources(BEHAVIOR).getResources().stream() + .filter(r -> automationScriptIds.contains(r.getScriptId())).toList(); } catch (ApiException | AssetNotLoadedException e) { - if (logger.isDebugEnabled()) { - logger.debug("updateChannels() unexpected exception", e); - } else { - logger.warn("Unexpected exception '{}' while updating channels.", e.getMessage()); - } + logger.warn("Unexpected exception '{}' while updating channels.", e.getMessage(), + logger.isDebugEnabled() ? e : null); return; } catch (InterruptedException e) { return; } - List automations = getAutomationsList(resources); - if (automations.size() != automationIds.size() || automations.stream().anyMatch(a -> !automationIds.containsKey(a.getId())) || automations.stream().anyMatch(a -> !a.getName().equals(automationIds.get(a.getId())))) { @@ -875,8 +892,10 @@ private Channel createAutomationChannel(Resource automation) { * Process event resources list and update the automation channels */ public void onResources(List resources) { + List automations = resources.stream().filter(r -> automationScriptIds.contains(r.getScriptId())) + .toList(); boolean refreshAutomationChannels = false; - for (Resource automation : getAutomationsList(resources)) { + for (Resource automation : automations) { String automationId = automation.getId(); refreshAutomationChannels |= !automationIds.containsKey(automationId); ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, automationId); @@ -888,11 +907,4 @@ public void onResources(List resources) { channelRefreshTask = scheduler.submit(() -> updateChannels()); } } - - /** - * Create a filtered resource list that contains only automation resources - */ - private List getAutomationsList(Collection resources) { - return resources.stream().filter(r -> r.isAutomationResource()).toList(); - } } From 9f4bbae1505b03cc193c31eb2a764f7c309bbdcc Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Sun, 18 Aug 2024 18:48:47 +0100 Subject: [PATCH 19/20] fix channel renaming/adding/deleting, and event updating Signed-off-by: AndrewFG --- .../hue/internal/api/dto/clip2/Event.java | 7 ++ .../hue/internal/api/dto/clip2/Resource.java | 40 ++++-- .../api/dto/clip2/enums/ContentType.java | 40 ++++++ .../hue/internal/connection/Clip2Bridge.java | 7 +- .../internal/handler/Clip2BridgeHandler.java | 119 +++++++++++------- .../hue/internal/clip2/SettersTest.java | 23 ++-- 6 files changed, 166 insertions(+), 70 deletions(-) create mode 100644 bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java index 7a27ff721fa03..0b45370f9c113 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java @@ -19,7 +19,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType; +import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; /** @@ -33,6 +35,11 @@ public class Event { }.getType(); private @Nullable List data = new ArrayList<>(); + private @Nullable @SerializedName("type") String contentType; // content type of resources + + public ContentType getContentType() { + return ContentType.of(contentType); + } public List getData() { List data = this.data; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index ddf86d2e2c8ad..6975614df42ea 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -30,6 +30,7 @@ import org.openhab.binding.hue.internal.api.dto.clip2.enums.ButtonEventType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.CategoryType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContactStateType; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.SceneRecallAction; @@ -76,8 +77,15 @@ public class Resource { * values have changed. A sparse resource does not contain the full state of the resource. And the absence of any * field from such a resource does not indicate that the field value is UNDEF, but rather that the value is the same * as what it was previously set to by the last non-sparse resource. + *

+ * The following content types are defined: + * + *

  • ADD resource being added; contains (assumed) all fields
  • + *
  • DELETE resource being deleted; contains id and type only
  • + *
  • UPDATE resource being updated; contains id, type and changed fields
  • + *
  • FULL_STATE existing resource being downloaded; contains all fields
  • */ - private transient boolean hasSparseData; + private transient ContentType contentType; private @Nullable String type; private @Nullable String id; @@ -112,12 +120,20 @@ public class Resource { private @Nullable JsonElement state; private @Nullable @SerializedName("script_id") String scriptId; + /** + * Constructor + */ + public Resource() { + contentType = ContentType.FULL_STATE; + } + /** * Constructor * * @param resourceType */ public Resource(@Nullable ResourceType resourceType) { + this(); if (Objects.nonNull(resourceType)) { setType(resourceType); } @@ -386,6 +402,10 @@ public State getContactState() { : OpenClosedType.OPEN; } + public ContentType getContentType() { + return contentType; + } + public int getControlId() { MetaData metadata = this.metadata; return Objects.nonNull(metadata) ? metadata.getControlId() : 0; @@ -804,17 +824,12 @@ public State getZigbeeState() { } public boolean hasFullState() { - return !hasSparseData; + return ContentType.FULL_STATE == contentType; } - /** - * Mark that the resource has sparse data. - * - * @return this instance. - */ - public Resource markAsSparse() { - hasSparseData = true; - return this; + public boolean hasName() { + MetaData metaData = getMetaData(); + return Objects.nonNull(metaData) && Objects.nonNull(metaData.getName()); } public Resource setAlerts(Alerts alert) { @@ -837,6 +852,11 @@ public Resource setContactReport(ContactReport contactReport) { return this; } + public Resource setContentType(ContentType contentType) { + this.contentType = contentType; + return this; + } + public Resource setDimming(@Nullable Dimming dimming) { this.dimming = dimming; return this; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java new file mode 100644 index 0000000000000..013a435d35be4 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java @@ -0,0 +1,40 @@ +/** + * 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.hue.internal.api.dto.clip2.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Enum for content type of Resource instances + * + * @author Andrew Fiddian-Green - Initial contribution + */ +@NonNullByDefault +public enum ContentType { + ADD, // resource being added; contains (maybe) all fields + DELETE, // resource being deleted; contains id and type only + UPDATE, // resource being updated; contains id, type and updated fields + FULL_STATE; // existing resource being downloaded; contains all fields + + public static ContentType of(@Nullable String value) { + if (value != null) { + try { + return valueOf(value.toUpperCase()); + } catch (IllegalArgumentException e) { + // fall through + } + } + return UPDATE; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java index 69d7fa67caa21..13230ac9510f6 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java @@ -921,12 +921,15 @@ protected void onEventData(String data) { return; } List resources = new ArrayList<>(); - events.forEach(event -> resources.addAll(event.getData())); + events.forEach(event -> { + List eventResources = event.getData(); + eventResources.forEach(resource -> resource.setContentType(event.getContentType())); + resources.addAll(eventResources); + }); if (resources.isEmpty()) { LOGGER.debug("onEventData() resource list is empty"); return; } - resources.forEach(resource -> resource.markAsSparse()); bridgeHandler.onResourcesEvent(resources); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 5b5b55ef9fcea..395835a7fad22 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -55,7 +54,6 @@ import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.TlsTrustManagerProvider; import org.openhab.core.library.CoreItemFactory; -import org.openhab.core.library.types.OnOffType; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; @@ -73,8 +71,6 @@ import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; -import org.openhab.core.types.UnDefType; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceRegistration; @@ -123,15 +119,15 @@ public class Clip2BridgeHandler extends BaseBridgeHandler { private final Bundle bundle; private final LocaleProvider localeProvider; private final TranslationProvider translationProvider; - private final Map automationIds = new ConcurrentHashMap<>();; - private final Set automationScriptIds = new HashSet<>(); + private final Map automationsCache = new ConcurrentHashMap<>();; + private final Set automationScriptIds = ConcurrentHashMap.newKeySet(); private final ChannelGroupUID automationChannelGroupUID; private @Nullable Clip2Bridge clip2Bridge; private @Nullable ServiceRegistration trustManagerRegistration; private @Nullable Clip2ThingDiscoveryService discoveryService; - private @Nullable Future channelRefreshTask; + private @Nullable Future updateAutomationChannelsTask; private @Nullable Future checkConnectionTask; private @Nullable Future updateOnlineStateTask; private @Nullable ScheduledFuture scheduledUpdateTask; @@ -286,11 +282,11 @@ private void disposeAssets() { logger.debug("disposeAssets() {}", this); synchronized (this) { assetsLoaded = false; - cancelTask(channelRefreshTask, true); + cancelTask(updateAutomationChannelsTask, true); cancelTask(checkConnectionTask, true); cancelTask(updateOnlineStateTask, true); cancelTask(scheduledUpdateTask, true); - channelRefreshTask = null; + updateAutomationChannelsTask = null; checkConnectionTask = null; updateOnlineStateTask = null; scheduledUpdateTask = null; @@ -443,18 +439,16 @@ public Collection> getServices() { public void handleCommand(ChannelUID channelUID, Command command) { if (CHANNEL_GROUP_AUTOMATION.equals(channelUID.getGroupId())) { try { - Resources resources; if (RefreshType.REFRESH.equals(command)) { - resources = getClip2Bridge().getResources(new ResourceReference() - .setType(ResourceType.BEHAVIOR_INSTANCE).setId(channelUID.getIdWithoutGroup())); - onResources(resources.getResources()); + updateAutomationChannelsNow(); + return; } else { - resources = getClip2Bridge().putResource(new Resource(ResourceType.BEHAVIOR_INSTANCE) + Resources resources = getClip2Bridge().putResource(new Resource(ResourceType.BEHAVIOR_INSTANCE) .setId(channelUID.getIdWithoutGroup()).setEnabled(command)); - } - if (resources.hasErrors()) { - logger.warn("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, - String.join("; ", resources.getErrors())); + if (resources.hasErrors()) { + logger.warn("handleCommand({}, {}) succeeded with errors: {}", channelUID, command, + String.join("; ", resources.getErrors())); + } } } catch (ApiException | AssetNotLoadedException e) { logger.warn("handleCommand({}, {}) error {}", channelUID, command, e.getMessage(), @@ -567,13 +561,15 @@ public void onResourcesEvent(List resources) { } private void onResourcesEventTask(List resources) { - onResources(resources); int numberOfResources = resources.size(); logger.debug("onResourcesEventTask() resource count {}", numberOfResources); Setters.mergeLightResources(resources); if (numberOfResources != resources.size()) { logger.debug("onResourcesEventTask() merged to {} resources", resources.size()); } + if (onResources(resources)) { + updateAutomationChannelsNow(); + } getThing().getThings().forEach(thing -> { if (thing.getHandler() instanceof Clip2ThingHandler clip2ThingHandler) { clip2ThingHandler.onResources(resources); @@ -640,6 +636,7 @@ private void updateOnlineState() { connectRetriesRemaining = RECONNECT_MAX_TRIES; updateStatus(ThingStatus.ONLINE); loadAutomationScriptIds(); + updateAutomationChannelsNow(); updateThingsScheduled(500); Clip2ThingDiscoveryService discoveryService = this.discoveryService; if (Objects.nonNull(discoveryService)) { @@ -794,8 +791,6 @@ private void updateThingsNow() { } }); } - - updateChannels(); } catch (ApiException | AssetNotLoadedException e) { if (logger.isDebugEnabled()) { logger.debug("updateThingsNow() unexpected exception", e); @@ -825,10 +820,12 @@ private void updateThingsScheduled(int delayMilliSeconds) { */ private void loadAutomationScriptIds() { try { - automationScriptIds.clear(); - automationScriptIds.addAll(getClip2Bridge().getResources(SCRIPT).getResources().stream() - .filter(r -> CategoryType.AUTOMATION == r.getCategory()).map(r -> r.getId()) - .collect(Collectors.toSet())); + synchronized (this) { + automationScriptIds.clear(); + automationScriptIds.addAll(getClip2Bridge().getResources(SCRIPT).getResources().stream() + .filter(r -> CategoryType.AUTOMATION == r.getCategory()).map(r -> r.getId()) + .collect(Collectors.toSet())); + } } catch (ApiException | AssetNotLoadedException e) { logger.warn("loadAutomationScriptIds() unexpected exception {}", e.getMessage(), logger.isDebugEnabled() ? e : null); @@ -837,9 +834,9 @@ private void loadAutomationScriptIds() { } /** - * Create the automation channels + * Create resp. update the automation channels */ - private void updateChannels() { + private void updateAutomationChannels() { List automations; try { automations = getClip2Bridge().getResources(BEHAVIOR).getResources().stream() @@ -852,12 +849,15 @@ private void updateChannels() { return; } - if (automations.size() != automationIds.size() - || automations.stream().anyMatch(a -> !automationIds.containsKey(a.getId())) - || automations.stream().anyMatch(a -> !a.getName().equals(automationIds.get(a.getId())))) { + if (automations.size() != automationsCache.size() || automations.stream().anyMatch(automation -> { + Resource cachedAutomation = automationsCache.get(automation.getId()); + return Objects.isNull(cachedAutomation) || !automation.getName().equals(cachedAutomation.getName()); + })) { - automationIds.clear(); - automationIds.putAll(automations.stream().collect(Collectors.toMap(a -> a.getId(), a -> a.getName()))); + synchronized (this) { + automationsCache.clear(); + automationsCache.putAll(automations.stream().collect(Collectors.toMap(a -> a.getId(), a -> a))); + } Stream newChannels = automations.stream().map(a -> createAutomationChannel(a)); Stream oldchannels = thing.getChannels().stream() @@ -870,6 +870,14 @@ private void updateChannels() { } } + /** + * Start a task to update the automation channels + */ + private void updateAutomationChannelsNow() { + cancelTask(updateAutomationChannelsTask, false); + updateAutomationChannelsTask = scheduler.submit(() -> updateAutomationChannels()); + } + /** * Create an automation channel from an automation resource */ @@ -889,22 +897,39 @@ private Channel createAutomationChannel(Resource automation) { } /** - * Process event resources list and update the automation channels + * Process event resources list + * + * @return true if the automation channels require updating */ - public void onResources(List resources) { - List automations = resources.stream().filter(r -> automationScriptIds.contains(r.getScriptId())) - .toList(); - boolean refreshAutomationChannels = false; - for (Resource automation : automations) { - String automationId = automation.getId(); - refreshAutomationChannels |= !automationIds.containsKey(automationId); - ChannelUID channelUID = new ChannelUID(automationChannelGroupUID, automationId); - Boolean enabled = automation.getEnabled(); - State state = Objects.nonNull(enabled) ? OnOffType.from(enabled) : UnDefType.UNDEF; - updateState(channelUID, state); - } - if (refreshAutomationChannels) { - channelRefreshTask = scheduler.submit(() -> updateChannels()); + public boolean onResources(List resources) { + boolean requireUpdateChannels = false; + for (Resource resource : resources) { + if (ResourceType.BEHAVIOR_INSTANCE != resource.getType()) { + continue; + } + String resourceId = resource.getId(); + switch (resource.getContentType()) { + case ADD: + requireUpdateChannels |= automationScriptIds.contains(resource.getScriptId()); + break; + case DELETE: + requireUpdateChannels |= automationsCache.containsKey(resourceId); + break; + case UPDATE: + case FULL_STATE: + Resource cachedAutomation = automationsCache.get(resourceId); + if (Objects.isNull(cachedAutomation)) { + requireUpdateChannels |= automationScriptIds.contains(resource.getScriptId()); + } else { + if (resource.hasName() && !resource.getName().equals(cachedAutomation.getName())) { + requireUpdateChannels = true; + } else if (Objects.nonNull(resource.getEnabled())) { + updateState(new ChannelUID(automationChannelGroupUID, resourceId), + resource.getEnabledState()); + } + } + } } + return requireUpdateChannels; } } diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/SettersTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/SettersTest.java index d005cb906fbe6..2f5efa7a15c45 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/SettersTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/SettersTest.java @@ -30,13 +30,14 @@ import org.openhab.binding.hue.internal.api.dto.clip2.Effects; import org.openhab.binding.hue.internal.api.dto.clip2.OnState; import org.openhab.binding.hue.internal.api.dto.clip2.Resource; +import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType; import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType; import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters; import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; /** * Tests for {@link Setters}. - * + * * @author Jacob Laursen - Initial contribution */ @NonNullByDefault @@ -51,7 +52,7 @@ public class SettersTest { * * Expected output: * - Resource 1: type=light/grouped_light, sparse, id=1, on=on, dimming=50 - * + * * @throws DTOPresentButEmptyException */ @ParameterizedTest @@ -100,7 +101,7 @@ private static Stream provideLightResourceTypes() { * * Expected output: * - Resource 1: type=light, sparse, id=1, dimming=50 - * + * * @throws DTOPresentButEmptyException */ @Test @@ -137,7 +138,7 @@ void mergeLightResourcesMergeDimmingToLatestValueWhenSparseAndSameId() throws DT * Expected output: * - Resource 1: type=light, sparse, id=1, on=on, dimming=50 * - Resource 2: type=light, sparse, id=1, effect=xxx - * + * * @throws DTOPresentButEmptyException */ @Test @@ -185,7 +186,7 @@ void mergeLightResourcesMergeHSBFieldsDoNotRemoveResourceWithEffect() throws DTO * Expected output: * - Resource 1: type=light, sparse, id=1, on=on * - Resource 2: type=light, sparse, id=2, dimming=50 - * + * * @throws DTOPresentButEmptyException */ @Test @@ -228,7 +229,7 @@ void mergeLightResourcesDoNotMergeOnStateAndDimmingWhenSparseAndDifferentId() th * * Expected output: * - Exception thrown, full state is not supported/expected. - * + * * @throws DTOPresentButEmptyException */ @Test @@ -254,7 +255,7 @@ void mergeLightResourcesMergeOnStateAndDimmingWhenFullStateFirstAndSameId() thro * Expected output: * - Resource 1: type=light, sparse, id=1, on=on * - Resource 2: type=light, sparse, id=1, color temperature=370 mirek - * + * * @throws DTOPresentButEmptyException */ @Test @@ -301,7 +302,7 @@ void mergeLightResourcesDoNotMergeOnStateAndColorTemperatureWhenSparseAndSameId( * Expected output: * - Resource 1: type=light, sparse, id=1, on=on, dimming=50 * - Resource 2: type=light, sparse, id=1, color temperature=370 mirek - * + * * @throws DTOPresentButEmptyException */ @Test @@ -352,7 +353,7 @@ void mergeLightResourcesMergeOnStateAndDimmingButNotColorTemperatureWhenSparseAn * * Expected output: * - Resource 1: type=light, sparse, id=1, on=on, color temperature=370 - * + * * @throws DTOPresentButEmptyException */ @Test @@ -389,7 +390,7 @@ void mergeLightResourcesSeparateOnStateAndColorTemperatureWhenSparseAndSameId() * * Expected output: * - Resource 1: type=motion, sparse, id=1 - * + * * @throws DTOPresentButEmptyException */ @Test @@ -431,7 +432,7 @@ private ColorTemperature createColorTemperature(double mirek) { private Resource createResource(ResourceType resourceType, String id) { Resource resource = new Resource(resourceType); resource.setId(id); - resource.markAsSparse(); + resource.setContentType(ContentType.UPDATE); return resource; } From 8a849d4bef781c9e679a2ddecbc1237d50638b16 Mon Sep 17 00:00:00 2001 From: AndrewFG Date: Mon, 19 Aug 2024 11:17:32 +0100 Subject: [PATCH 20/20] adopt reviewer suggestions Signed-off-by: AndrewFG --- .../hue/internal/api/dto/clip2/Event.java | 8 +++--- .../hue/internal/api/dto/clip2/Resource.java | 1 + .../api/dto/clip2/enums/ContentType.java | 28 ++++++++----------- .../internal/handler/Clip2BridgeHandler.java | 6 ++-- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java index 0b45370f9c113..9712bb593c758 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Event.java @@ -13,7 +13,6 @@ package org.openhab.binding.hue.internal.api.dto.clip2; import java.lang.reflect.Type; -import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -34,11 +33,12 @@ public class Event { public static final Type EVENT_LIST_TYPE = new TypeToken>() { }.getType(); - private @Nullable List data = new ArrayList<>(); - private @Nullable @SerializedName("type") String contentType; // content type of resources + private @Nullable List data; + private @Nullable @SerializedName("type") ContentType contentType; // content type of resources public ContentType getContentType() { - return ContentType.of(contentType); + ContentType contentType = this.contentType; + return Objects.nonNull(contentType) ? contentType : ContentType.ERROR; } public List getData() { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java index 6975614df42ea..8adb111a2d4b0 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/Resource.java @@ -83,6 +83,7 @@ public class Resource { *
  • ADD resource being added; contains (assumed) all fields
  • *
  • DELETE resource being deleted; contains id and type only
  • *
  • UPDATE resource being updated; contains id, type and changed fields
  • + *
  • ERROR resource with error; contents unknown
  • *
  • FULL_STATE existing resource being downloaded; contains all fields
  • */ private transient ContentType contentType; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java index 013a435d35be4..4e3901f0b3822 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/api/dto/clip2/enums/ContentType.java @@ -13,7 +13,8 @@ package org.openhab.binding.hue.internal.api.dto.clip2.enums; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; /** * Enum for content type of Resource instances @@ -22,19 +23,14 @@ */ @NonNullByDefault public enum ContentType { - ADD, // resource being added; contains (maybe) all fields - DELETE, // resource being deleted; contains id and type only - UPDATE, // resource being updated; contains id, type and updated fields - FULL_STATE; // existing resource being downloaded; contains all fields - - public static ContentType of(@Nullable String value) { - if (value != null) { - try { - return valueOf(value.toUpperCase()); - } catch (IllegalArgumentException e) { - // fall through - } - } - return UPDATE; - } + @SerializedName("add") // resource being added; contains (maybe) all fields + ADD, + @SerializedName("delete") // resource being deleted; contains id and type only + DELETE, + @SerializedName("update") // resource being updated; contains id, type and updated fields + UPDATE, + @SerializedName("error") // resource error event + ERROR, + // existing resource being downloaded; contains all fields; excluded from (de-)serialization + FULL_STATE } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java index 395835a7fad22..37f7c6e8f7981 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2BridgeHandler.java @@ -820,7 +820,7 @@ private void updateThingsScheduled(int delayMilliSeconds) { */ private void loadAutomationScriptIds() { try { - synchronized (this) { + synchronized (automationScriptIds) { automationScriptIds.clear(); automationScriptIds.addAll(getClip2Bridge().getResources(SCRIPT).getResources().stream() .filter(r -> CategoryType.AUTOMATION == r.getCategory()).map(r -> r.getId()) @@ -854,7 +854,7 @@ private void updateAutomationChannels() { return Objects.isNull(cachedAutomation) || !automation.getName().equals(cachedAutomation.getName()); })) { - synchronized (this) { + synchronized (automationsCache) { automationsCache.clear(); automationsCache.putAll(automations.stream().collect(Collectors.toMap(a -> a.getId(), a -> a))); } @@ -928,6 +928,8 @@ public boolean onResources(List resources) { resource.getEnabledState()); } } + break; + default: } } return requireUpdateChannels;