2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hue.internal.handler;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Comparator;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
27 import java.util.Objects;
29 import java.util.TreeSet;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.CopyOnWriteArrayList;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.TimeUnit;
34 import java.util.stream.Collectors;
35 import java.util.stream.Stream;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.hue.internal.action.DynamicsActions;
40 import org.openhab.binding.hue.internal.api.dto.clip2.Alerts;
41 import org.openhab.binding.hue.internal.api.dto.clip2.ColorXy;
42 import org.openhab.binding.hue.internal.api.dto.clip2.Dimming;
43 import org.openhab.binding.hue.internal.api.dto.clip2.Effects;
44 import org.openhab.binding.hue.internal.api.dto.clip2.Gamut2;
45 import org.openhab.binding.hue.internal.api.dto.clip2.MetaData;
46 import org.openhab.binding.hue.internal.api.dto.clip2.MirekSchema;
47 import org.openhab.binding.hue.internal.api.dto.clip2.ProductData;
48 import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
49 import org.openhab.binding.hue.internal.api.dto.clip2.ResourceReference;
50 import org.openhab.binding.hue.internal.api.dto.clip2.Resources;
51 import org.openhab.binding.hue.internal.api.dto.clip2.TimedEffects;
52 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ActionType;
53 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ContentType;
54 import org.openhab.binding.hue.internal.api.dto.clip2.enums.EffectType;
55 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
56 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SceneRecallAction;
57 import org.openhab.binding.hue.internal.api.dto.clip2.enums.SmartSceneRecallAction;
58 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ZigbeeStatus;
59 import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters;
60 import org.openhab.binding.hue.internal.config.Clip2ThingConfig;
61 import org.openhab.binding.hue.internal.exceptions.ApiException;
62 import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
63 import org.openhab.core.i18n.TimeZoneProvider;
64 import org.openhab.core.library.types.DateTimeType;
65 import org.openhab.core.library.types.DecimalType;
66 import org.openhab.core.library.types.HSBType;
67 import org.openhab.core.library.types.IncreaseDecreaseType;
68 import org.openhab.core.library.types.OnOffType;
69 import org.openhab.core.library.types.PercentType;
70 import org.openhab.core.library.types.QuantityType;
71 import org.openhab.core.library.types.StringType;
72 import org.openhab.core.library.unit.MetricPrefix;
73 import org.openhab.core.library.unit.Units;
74 import org.openhab.core.thing.Bridge;
75 import org.openhab.core.thing.Channel;
76 import org.openhab.core.thing.ChannelUID;
77 import org.openhab.core.thing.Thing;
78 import org.openhab.core.thing.ThingRegistry;
79 import org.openhab.core.thing.ThingStatus;
80 import org.openhab.core.thing.ThingStatusDetail;
81 import org.openhab.core.thing.ThingTypeUID;
82 import org.openhab.core.thing.ThingUID;
83 import org.openhab.core.thing.binding.BaseThingHandler;
84 import org.openhab.core.thing.binding.BridgeHandler;
85 import org.openhab.core.thing.binding.ThingHandlerService;
86 import org.openhab.core.thing.binding.builder.ChannelBuilder;
87 import org.openhab.core.thing.binding.builder.ThingBuilder;
88 import org.openhab.core.thing.link.ItemChannelLink;
89 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
90 import org.openhab.core.thing.type.ChannelTypeUID;
91 import org.openhab.core.types.Command;
92 import org.openhab.core.types.RefreshType;
93 import org.openhab.core.types.State;
94 import org.openhab.core.types.StateOption;
95 import org.openhab.core.types.UnDefType;
96 import org.slf4j.Logger;
97 import org.slf4j.LoggerFactory;
100 * Handler for things based on CLIP 2 'device', 'room', or 'zone resources.
102 * @author Andrew Fiddian-Green - Initial contribution.
105 public class Clip2ThingHandler extends BaseThingHandler {
107 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM,
110 private static final Set<ResourceType> SUPPORTED_SCENE_TYPES = Set.of(ResourceType.SCENE, ResourceType.SMART_SCENE);
112 private static final Duration DYNAMICS_ACTIVE_WINDOW = Duration.ofSeconds(10);
114 private static final String LK_WISER_DIMMER_MODEL_ID = "LK Dimmer";
116 private final Logger logger = LoggerFactory.getLogger(Clip2ThingHandler.class);
118 // flag values for logging resource consumption
119 private static final int FLAG_PROPERTIES_UPDATE = 1;
120 private static final int FLAG_DEPENDENCIES_UPDATE = 2;
121 private static final int FLAG_CACHE_UPDATE = 4;
122 private static final int FLAG_CHANNELS_UPDATE = 8;
123 private static final int FLAG_SCENE_ADD = 16;
124 private static final int FLAG_SCENE_DELETE = 32;
127 * A map of service Resources whose state contributes to the overall state of this thing. It is a map between the
128 * resource ID (string) and a Resource object containing the last known state. e.g. a DEVICE thing may support a
129 * LIGHT service whose Resource contributes to its overall state, or a ROOM or ZONE thing may support a
130 * GROUPED_LIGHT service whose Resource contributes to the its overall state.
132 private final Map<String, Resource> serviceContributorsCache = new ConcurrentHashMap<>();
135 * A map of Resource IDs which are targets for commands to be sent. It is a map between the type of command
136 * (ResourcesType) and the resource ID to which the command shall be sent. e.g. a LIGHT 'on' command shall be sent
137 * to the respective LIGHT resource ID.
139 private final Map<ResourceType, String> commandResourceIds = new ConcurrentHashMap<>();
142 * Button devices contain one or more physical buttons, each of which is represented by a BUTTON Resource with its
143 * own unique resource ID, and a respective controlId that indicates which button it is in the device. e.g. a dimmer
144 * pad has four buttons (controlId's 1..4) each represented by a BUTTON Resource with a unique resource ID. This is
145 * a map between the resource ID and its respective controlId.
147 private final Map<String, Integer> controlIds = new ConcurrentHashMap<>();
150 * The set of channel IDs that are supported by this thing. e.g. an on/off light may support 'switch' and
151 * 'zigbeeStatus' channels, whereas a complex light may support 'switch', 'brightness', 'color', 'color temperature'
152 * and 'zigbeeStatus' channels.
154 private final Set<String> supportedChannelIdSet = new HashSet<>();
157 * A map of scene IDs versus scene Resources for the scenes that contribute to and command this thing. It is a map
158 * between the resource ID (string) and a Resource object containing the scene's last known state.
160 private final Map<String, Resource> sceneContributorsCache = new ConcurrentHashMap<>();
163 * A map of scene names versus scene Resources for the scenes that contribute to and command this thing. e.g. a
164 * command for a scene named 'Energize' shall be sent to the respective SCENE resource ID.
166 private final Map<String, Resource> sceneResourceEntries = new ConcurrentHashMap<>();
169 * A list of API v1 thing channel UIDs that are linked to items. It is used in the process of replicating the
170 * Item/Channel links from a legacy v1 thing to this API v2 thing.
172 private final List<ChannelUID> legacyLinkedChannelUIDs = new CopyOnWriteArrayList<>();
174 private final ThingRegistry thingRegistry;
175 private final ItemChannelLinkRegistry itemChannelLinkRegistry;
176 private final Clip2StateDescriptionProvider stateDescriptionProvider;
177 private final TimeZoneProvider timeZoneProvider;
179 private String resourceId = "?";
180 private Resource thisResource;
181 private Duration dynamicsDuration = Duration.ZERO;
182 private Instant dynamicsExpireTime = Instant.MIN;
183 private Instant buttonGroupLastUpdated = Instant.MIN;
185 private boolean disposing;
186 private boolean hasConnectivityIssue;
187 private boolean updateSceneContributorsDone;
188 private boolean updateLightPropertiesDone;
189 private boolean updatePropertiesDone;
190 private boolean updateDependenciesDone;
191 private boolean applyOffTransitionWorkaround;
193 private @Nullable Future<?> alertResetTask;
194 private @Nullable Future<?> dynamicsResetTask;
195 private @Nullable Future<?> updateDependenciesTask;
196 private @Nullable Future<?> updateServiceContributorsTask;
198 public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescriptionProvider,
199 TimeZoneProvider timeZoneProvider, ThingRegistry thingRegistry,
200 ItemChannelLinkRegistry itemChannelLinkRegistry) {
203 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
204 if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
205 thisResource = new Resource(ResourceType.DEVICE);
206 } else if (THING_TYPE_ROOM.equals(thingTypeUID)) {
207 thisResource = new Resource(ResourceType.ROOM);
208 } else if (THING_TYPE_ZONE.equals(thingTypeUID)) {
209 thisResource = new Resource(ResourceType.ZONE);
211 throw new IllegalArgumentException("Wrong thing type " + thingTypeUID.getAsString());
214 this.thingRegistry = thingRegistry;
215 this.itemChannelLinkRegistry = itemChannelLinkRegistry;
216 this.stateDescriptionProvider = stateDescriptionProvider;
217 this.timeZoneProvider = timeZoneProvider;
221 * Add a channel ID to the supportedChannelIdSet set. If the channel supports dynamics (timed transitions) then add
222 * the respective channel as well.
224 * @param channelId the channel ID to add.
226 private void addSupportedChannel(String channelId) {
227 if (!disposing && !updateDependenciesDone) {
228 synchronized (supportedChannelIdSet) {
229 logger.debug("{} -> addSupportedChannel() '{}' added to supported channel set", resourceId, channelId);
230 supportedChannelIdSet.add(channelId);
231 if (DYNAMIC_CHANNELS.contains(channelId)) {
232 clearDynamicsChannel();
239 * Cancel the given task.
241 * @param cancelTask the task to be cancelled (may be null)
242 * @param mayInterrupt allows cancel() to interrupt the thread.
244 private void cancelTask(@Nullable Future<?> cancelTask, boolean mayInterrupt) {
245 if (Objects.nonNull(cancelTask)) {
246 cancelTask.cancel(mayInterrupt);
251 * Clear the dynamics channel parameters.
253 private void clearDynamicsChannel() {
254 dynamicsExpireTime = Instant.MIN;
255 dynamicsDuration = Duration.ZERO;
256 updateState(CHANNEL_2_DYNAMICS, new QuantityType<>(0, MetricPrefix.MILLI(Units.SECOND)), true);
260 public void dispose() {
261 logger.debug("{} -> dispose()", resourceId);
263 cancelTask(alertResetTask, true);
264 cancelTask(dynamicsResetTask, true);
265 cancelTask(updateDependenciesTask, true);
266 cancelTask(updateServiceContributorsTask, true);
267 alertResetTask = null;
268 dynamicsResetTask = null;
269 updateDependenciesTask = null;
270 updateServiceContributorsTask = null;
271 legacyLinkedChannelUIDs.clear();
272 sceneContributorsCache.clear();
273 sceneResourceEntries.clear();
274 supportedChannelIdSet.clear();
275 commandResourceIds.clear();
276 serviceContributorsCache.clear();
281 * Get the bridge handler.
283 * @throws AssetNotLoadedException if the handler does not exist.
285 private Clip2BridgeHandler getBridgeHandler() throws AssetNotLoadedException {
286 Bridge bridge = getBridge();
287 if (Objects.nonNull(bridge)) {
288 BridgeHandler handler = bridge.getHandler();
289 if (handler instanceof Clip2BridgeHandler) {
290 return (Clip2BridgeHandler) handler;
293 throw new AssetNotLoadedException("Bridge handler missing");
297 * Do a double lookup to get the cached resource that matches the given ResourceType.
299 * @param resourceType the type to search for.
300 * @return the Resource, or null if not found.
302 private @Nullable Resource getCachedResource(ResourceType resourceType) {
303 String commandResourceId = commandResourceIds.get(resourceType);
304 return Objects.nonNull(commandResourceId) ? serviceContributorsCache.get(commandResourceId) : null;
308 * Return a ResourceReference to this handler's resource.
310 * @return a ResourceReference instance.
312 public ResourceReference getResourceReference() {
313 return new ResourceReference().setId(resourceId).setType(thisResource.getType());
317 * Register the 'DynamicsAction' service.
320 public Collection<Class<? extends ThingHandlerService>> getServices() {
321 return Set.of(DynamicsActions.class);
325 public void handleCommand(ChannelUID channelUID, Command commandParam) {
326 if (RefreshType.REFRESH.equals(commandParam)) {
327 if (thing.getStatus() == ThingStatus.ONLINE) {
328 refreshAllChannels();
333 Channel channel = thing.getChannel(channelUID);
334 if (channel == null) {
335 if (logger.isDebugEnabled()) {
336 logger.debug("{} -> handleCommand() channelUID:{} does not exist", resourceId, channelUID);
339 logger.warn("Command received for channel '{}' which is not in thing '{}'.", channelUID,
345 ResourceType lightResourceType = thisResource.getType() == ResourceType.DEVICE ? ResourceType.LIGHT
346 : ResourceType.GROUPED_LIGHT;
348 Resource putResource = null;
349 String putResourceId = null;
350 Command command = commandParam;
351 String channelId = channelUID.getId();
352 Resource cache = getCachedResource(lightResourceType);
355 case CHANNEL_2_ALERT:
356 putResource = Setters.setAlert(new Resource(lightResourceType), command, cache);
357 cancelTask(alertResetTask, false);
358 alertResetTask = scheduler.schedule(
359 () -> updateState(channelUID, new StringType(ActionType.NO_ACTION.name())), 10,
363 case CHANNEL_2_EFFECT:
364 putResource = Setters.setEffect(new Resource(lightResourceType), command, cache).setOnOff(OnOffType.ON);
367 case CHANNEL_2_COLOR_TEMP_PERCENT:
368 if (command instanceof IncreaseDecreaseType increaseDecreaseCommand && Objects.nonNull(cache)) {
369 command = translateIncreaseDecreaseCommand(increaseDecreaseCommand,
370 cache.getColorTemperaturePercentState());
371 } else if (command instanceof OnOffType) {
372 command = OnOffType.OFF == command ? PercentType.ZERO : PercentType.HUNDRED;
374 putResource = Setters.setColorTemperaturePercent(new Resource(lightResourceType), command, cache);
377 case CHANNEL_2_COLOR_TEMP_ABSOLUTE:
378 putResource = Setters.setColorTemperatureAbsolute(new Resource(lightResourceType), command, cache);
381 case CHANNEL_2_COLOR:
382 putResource = new Resource(lightResourceType);
383 if (command instanceof HSBType) {
384 HSBType color = ((HSBType) command);
385 putResource = Setters.setColorXy(putResource, color, cache);
386 command = color.getBrightness();
388 // NB fall through for handling of brightness and switch related commands !!
390 case CHANNEL_2_BRIGHTNESS:
391 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
392 if (command instanceof IncreaseDecreaseType increaseDecreaseCommand && Objects.nonNull(cache)) {
393 command = translateIncreaseDecreaseCommand(increaseDecreaseCommand, cache.getBrightnessState());
395 if (command instanceof PercentType) {
396 PercentType brightness = (PercentType) command;
397 putResource = Setters.setDimming(putResource, brightness, cache);
398 Double minDimLevel = Objects.nonNull(cache) ? cache.getMinimumDimmingLevel() : null;
399 minDimLevel = Objects.nonNull(minDimLevel) ? minDimLevel : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL;
400 command = OnOffType.from(brightness.doubleValue() >= minDimLevel);
402 // NB fall through for handling of switch related commands !!
404 case CHANNEL_2_SWITCH:
405 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
406 putResource.setOnOff(command);
407 applyDeviceSpecificWorkArounds(command, putResource);
410 case CHANNEL_2_COLOR_XY_ONLY:
411 putResource = Setters.setColorXy(new Resource(lightResourceType), command, cache);
414 case CHANNEL_2_DIMMING_ONLY:
415 putResource = Setters.setDimming(new Resource(lightResourceType), command, cache);
418 case CHANNEL_2_ON_OFF_ONLY:
419 putResource = new Resource(lightResourceType).setOnOff(command);
420 applyDeviceSpecificWorkArounds(command, putResource);
423 case CHANNEL_2_TEMPERATURE_ENABLED:
424 putResource = new Resource(ResourceType.TEMPERATURE).setEnabled(command);
427 case CHANNEL_2_MOTION_ENABLED:
428 putResource = new Resource(ResourceType.MOTION).setEnabled(command);
431 case CHANNEL_2_LIGHT_LEVEL_ENABLED:
432 putResource = new Resource(ResourceType.LIGHT_LEVEL).setEnabled(command);
435 case CHANNEL_2_SECURITY_CONTACT_ENABLED:
436 putResource = new Resource(ResourceType.CONTACT).setEnabled(command);
439 case CHANNEL_2_SCENE:
440 if (command instanceof StringType) {
441 Resource scene = sceneResourceEntries.get(((StringType) command).toString());
442 if (Objects.nonNull(scene)) {
443 ResourceType putResourceType = scene.getType();
444 putResource = new Resource(putResourceType);
445 switch (putResourceType) {
447 putResource.setRecallAction(SceneRecallAction.ACTIVE);
450 putResource.setRecallAction(SmartSceneRecallAction.ACTIVATE);
453 logger.debug("{} -> handleCommand() type '{}' is not a supported scene type",
454 resourceId, putResourceType);
457 putResourceId = scene.getId();
462 case CHANNEL_2_DYNAMICS:
463 Duration clearAfter = Duration.ZERO;
464 if (command instanceof QuantityType<?>) {
465 QuantityType<?> durationMs = ((QuantityType<?>) command).toUnit(MetricPrefix.MILLI(Units.SECOND));
466 if (Objects.nonNull(durationMs) && durationMs.longValue() > 0) {
467 Duration duration = Duration.ofMillis(durationMs.longValue());
468 dynamicsDuration = duration;
469 dynamicsExpireTime = Instant.now().plus(DYNAMICS_ACTIVE_WINDOW);
470 clearAfter = DYNAMICS_ACTIVE_WINDOW;
471 logger.debug("{} -> handleCommand() dynamics setting {} valid for {}", resourceId, duration,
475 cancelTask(dynamicsResetTask, false);
476 dynamicsResetTask = scheduler.schedule(() -> clearDynamicsChannel(), clearAfter.toMillis(),
477 TimeUnit.MILLISECONDS);
481 if (logger.isDebugEnabled()) {
482 logger.debug("{} -> handleCommand() channelUID:{} unknown", resourceId, channelUID);
484 logger.warn("Command received for unknown channel '{}'.", channelUID);
489 if (putResource == null) {
490 if (logger.isDebugEnabled()) {
491 logger.debug("{} -> handleCommand() command:{} not supported on channelUID:{}", resourceId, command,
494 logger.warn("Command '{}' is not supported on channel '{}'.", command, channelUID);
499 putResourceId = Objects.nonNull(putResourceId) ? putResourceId : commandResourceIds.get(putResource.getType());
500 if (putResourceId == null) {
501 if (logger.isDebugEnabled()) {
503 "{} -> handleCommand() channelUID:{}, command:{}, putResourceType:{} => missing resource ID",
504 resourceId, channelUID, command, putResource.getType());
506 logger.warn("Command '{}' for channel '{}' cannot be processed by thing '{}'.", command, channelUID,
512 if (DYNAMIC_CHANNELS.contains(channelId)) {
513 if (Instant.now().isBefore(dynamicsExpireTime) && !dynamicsDuration.isZero()
514 && !dynamicsDuration.isNegative()) {
515 if (ResourceType.SCENE == putResource.getType()) {
516 putResource.setRecallDuration(dynamicsDuration);
517 } else if (CHANNEL_2_EFFECT == channelId) {
518 putResource.setTimedEffectsDuration(dynamicsDuration);
520 putResource.setDynamicsDuration(dynamicsDuration);
525 putResource.setId(putResourceId);
526 logger.debug("{} -> handleCommand() put resource {}", resourceId, putResource);
529 Resources resources = getBridgeHandler().putResource(putResource);
530 if (resources.hasErrors()) {
531 logger.info("Command '{}' for thing '{}', channel '{}' succeeded with errors: {}", command,
532 thing.getUID(), channelUID, String.join("; ", resources.getErrors()));
534 } catch (ApiException | AssetNotLoadedException e) {
535 if (logger.isDebugEnabled()) {
536 logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
538 logger.warn("Command '{}' for thing '{}', channel '{}' failed with error '{}'.", command,
539 thing.getUID(), channelUID, e.getMessage());
541 } catch (InterruptedException e) {
545 private Command translateIncreaseDecreaseCommand(IncreaseDecreaseType command, State currentValue) {
546 if (currentValue instanceof PercentType currentPercent) {
547 int delta = command == IncreaseDecreaseType.INCREASE ? 10 : -10;
548 double newPercent = Math.min(100.0, Math.max(0.0, currentPercent.doubleValue() + delta));
549 return new PercentType(new BigDecimal(newPercent, Resource.PERCENT_MATH_CONTEXT));
555 private void refreshAllChannels() {
556 if (!updateDependenciesDone) {
559 cancelTask(updateServiceContributorsTask, false);
560 updateServiceContributorsTask = scheduler.schedule(() -> {
562 updateServiceContributors();
563 } catch (ApiException | AssetNotLoadedException e) {
564 logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
565 } catch (InterruptedException e) {
567 }, 3, TimeUnit.SECONDS);
571 * Apply device specific work-arounds needed for given command.
573 * @param command the handled command.
574 * @param putResource the resource that will be adjusted if needed.
576 private void applyDeviceSpecificWorkArounds(Command command, Resource putResource) {
577 if (command == OnOffType.OFF && applyOffTransitionWorkaround) {
578 putResource.setDynamicsDuration(dynamicsDuration);
583 * Handle a 'dynamics' command for the given channel ID for the given dynamics duration.
585 * @param channelId the ID of the target channel.
586 * @param command the new target state.
587 * @param duration the transition duration.
589 public synchronized void handleDynamicsCommand(String channelId, Command command, QuantityType<?> duration) {
590 if (DYNAMIC_CHANNELS.contains(channelId)) {
591 Channel dynamicsChannel = thing.getChannel(CHANNEL_2_DYNAMICS);
592 Channel targetChannel = thing.getChannel(channelId);
593 if (Objects.nonNull(dynamicsChannel) && Objects.nonNull(targetChannel)) {
594 logger.debug("{} - handleDynamicsCommand() channelId:{}, command:{}, duration:{}", resourceId,
595 channelId, command, duration);
596 handleCommand(dynamicsChannel.getUID(), duration);
597 handleCommand(targetChannel.getUID(), command);
601 logger.warn("Dynamics command '{}' for thing '{}', channel '{}' and duration'{}' failed.", command,
602 thing.getUID(), channelId, duration);
606 public void initialize() {
607 Clip2ThingConfig config = getConfigAs(Clip2ThingConfig.class);
609 String resourceId = config.resourceId;
610 if (resourceId.isBlank()) {
611 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
612 "@text/offline.api2.conf-error.resource-id-missing");
615 thisResource.setId(resourceId);
616 this.resourceId = resourceId;
617 logger.debug("{} -> initialize()", resourceId);
619 updateThingFromLegacy();
620 updateStatus(ThingStatus.UNKNOWN);
622 dynamicsDuration = Duration.ZERO;
623 dynamicsExpireTime = Instant.MIN;
626 hasConnectivityIssue = false;
627 updatePropertiesDone = false;
628 updateDependenciesDone = false;
629 updateLightPropertiesDone = false;
630 updateSceneContributorsDone = false;
632 Bridge bridge = getBridge();
633 if (Objects.nonNull(bridge)) {
634 BridgeHandler bridgeHandler = bridge.getHandler();
635 if (bridgeHandler instanceof Clip2BridgeHandler) {
636 ((Clip2BridgeHandler) bridgeHandler).childInitialized();
642 * Update the channel state depending on new resources sent from the bridge.
644 * @param resources a collection of Resource objects containing the new state.
646 public void onResources(Collection<Resource> resources) {
647 boolean sceneActivated = resources.stream()
648 .anyMatch(r -> sceneContributorsCache.containsKey(r.getId())
649 && (Objects.requireNonNullElse(r.getSceneActive(), false)
650 || Objects.requireNonNullElse(r.getSmartSceneActive(), false)));
651 for (Resource resource : resources) {
652 // Skip scene deactivation when we have also received a scene activation.
653 boolean updateChannels = !sceneActivated || !sceneContributorsCache.containsKey(resource.getId())
654 || Objects.requireNonNullElse(resource.getSceneActive(), false)
655 || Objects.requireNonNullElse(resource.getSmartSceneActive(), false);
656 onResource(resource, updateChannels);
661 * Update the channel state depending on a new resource sent from the bridge.
663 * @param resource a Resource object containing the new state.
665 private void onResource(Resource resource) {
666 onResource(resource, true);
670 * Update the channel state depending on a new resource sent from the bridge.
672 * @param resource a Resource object containing the new state.
673 * @param updateChannels update channels (otherwise only update cache/properties).
675 private void onResource(Resource resource, boolean updateChannels) {
679 int resourceConsumedFlags = 0;
680 if (resourceId.equals(resource.getId())) {
681 if (resource.hasFullState()) {
682 thisResource = resource;
683 if (!updatePropertiesDone) {
684 updateProperties(resource);
685 resourceConsumedFlags = updatePropertiesDone ? FLAG_PROPERTIES_UPDATE : 0;
688 if (!updateDependenciesDone) {
689 resourceConsumedFlags |= FLAG_DEPENDENCIES_UPDATE;
690 cancelTask(updateDependenciesTask, false);
691 updateDependenciesTask = scheduler.submit(() -> updateDependencies());
694 if (SUPPORTED_SCENE_TYPES.contains(resource.getType())) {
695 resourceConsumedFlags = checkSceneResourceAddDelete(resource);
697 Resource cachedResource = getResourceFromCache(resource);
698 if (cachedResource != null) {
699 Setters.setResource(resource, cachedResource);
700 resourceConsumedFlags |= FLAG_CACHE_UPDATE;
701 resourceConsumedFlags |= updateChannels && updateChannels(resource) ? FLAG_CHANNELS_UPDATE : 0;
702 putResourceToCache(resource);
703 if (ResourceType.LIGHT == resource.getType() && !updateLightPropertiesDone) {
704 updateLightProperties(resource);
708 if (resourceConsumedFlags != 0) {
709 logger.debug("{} -> onResource() consumed resource {}, flags:{}", resourceId, resource,
710 resourceConsumedFlags);
715 * Check if a scene resource is of type 'ADD or 'DELETE' and either add it to, or delete it from, the two scene
716 * resource caches; and refresh the scene channel state description selection options.
718 * @param sceneResource the respective scene resource
719 * @return a flag value indicating if the scene was added or deleted
721 private int checkSceneResourceAddDelete(Resource sceneResource) {
722 switch (sceneResource.getContentType()) {
724 if (getResourceReference().equals(sceneResource.getGroup())) {
725 sceneResource.setContentType(ContentType.FULL_STATE);
726 sceneContributorsCache.put(sceneResource.getId(), sceneResource);
727 sceneResourceEntries.put(sceneResource.getName(), sceneResource);
728 updateSceneChannelStateDescription();
729 return FLAG_SCENE_ADD;
733 Resource deletedScene = sceneContributorsCache.remove(sceneResource.getId());
734 if (Objects.nonNull(deletedScene)) {
735 sceneResourceEntries.remove(deletedScene.getName());
736 updateSceneChannelStateDescription();
737 return FLAG_SCENE_DELETE;
744 private void putResourceToCache(Resource resource) {
745 if (SUPPORTED_SCENE_TYPES.contains(resource.getType())) {
746 sceneContributorsCache.put(resource.getId(), resource);
748 serviceContributorsCache.put(resource.getId(), resource);
752 private @Nullable Resource getResourceFromCache(Resource resource) {
753 return SUPPORTED_SCENE_TYPES.contains(resource.getType()) //
754 ? sceneContributorsCache.get(resource.getId())
755 : serviceContributorsCache.get(resource.getId());
759 * Update the thing internal state depending on a full list of resources sent from the bridge. If the resourceType
760 * is SCENE then call updateScenes(), otherwise if the resource refers to this thing, consume it via onResource() as
761 * any other resource, or else if the resourceType nevertheless matches the thing type, set the thing state offline.
763 * @param resourceType the type of the resources in the list.
764 * @param fullResources the full list of resources of the given type.
766 public void onResourcesList(ResourceType resourceType, List<Resource> fullResources) {
767 if (resourceType == ResourceType.SCENE) {
768 updateSceneContributors(fullResources);
770 fullResources.stream().filter(r -> resourceId.equals(r.getId())).findAny()
771 .ifPresentOrElse(r -> onResource(r), () -> {
772 if (resourceType == thisResource.getType()) {
773 logger.debug("{} -> onResourcesList() configuration error: unknown resourceId", resourceId);
774 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
775 "@text/offline.api2.gone.resource-id-unknown");
782 * Process the incoming Resource to initialize the alert channel.
784 * @param resource a Resource possibly with an Alerts element.
786 private void updateAlertChannel(Resource resource) {
787 Alerts alerts = resource.getAlerts();
788 if (Objects.nonNull(alerts)) {
789 List<StateOption> stateOptions = alerts.getActionValues().stream().map(action -> action.name())
790 .map(actionId -> new StateOption(actionId, actionId)).collect(Collectors.toList());
791 if (!stateOptions.isEmpty()) {
792 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_ALERT), stateOptions);
793 logger.debug("{} -> updateAlerts() found {} associated alerts", resourceId, stateOptions.size());
799 * If this v2 thing has a matching v1 legacy thing in the system, then for each channel in the v1 thing that
800 * corresponds to an equivalent channel in this v2 thing, and for all items that are linked to the v1 channel,
801 * create a new channel/item link between that item and the respective v2 channel in this thing.
803 private void updateChannelItemLinksFromLegacy() {
805 legacyLinkedChannelUIDs.forEach(legacyLinkedChannelUID -> {
806 String targetChannelId = REPLICATE_CHANNEL_ID_MAP.get(legacyLinkedChannelUID.getId());
807 if (Objects.nonNull(targetChannelId)) {
808 Channel targetChannel = thing.getChannel(targetChannelId);
809 if (Objects.nonNull(targetChannel)) {
810 ChannelUID uid = targetChannel.getUID();
811 itemChannelLinkRegistry.getLinkedItems(legacyLinkedChannelUID).forEach(linkedItem -> {
812 String item = linkedItem.getName();
813 if (!itemChannelLinkRegistry.isLinked(item, uid)) {
814 if (logger.isDebugEnabled()) {
816 "{} -> updateChannelItemLinksFromLegacy() item:{} linked to channel:{}",
817 resourceId, item, uid);
819 logger.info("Item '{}' linked to thing '{}' channel '{}'", item, thing.getUID(),
822 itemChannelLinkRegistry.add(new ItemChannelLink(item, uid));
828 legacyLinkedChannelUIDs.clear();
833 * Set the active list of channels by removing any that had initially been created by the thing XML declaration, but
834 * which in fact did not have data returned from the bridge i.e. channels which are not in the supportedChannelIdSet
836 * Also warn if there are channels in the supportedChannelIdSet set which are not in the thing.
838 * Adjusts the channel list so that only the highest level channel is available in the normal channel list. If a
839 * light supports the color channel, then it's brightness and switch can be commanded via the 'B' part of the HSB
840 * channel value. And if it supports the brightness channel the switch can be controlled via the brightness. So we
841 * can remove these lower level channels from the normal channel list.
843 * For more advanced applications, it is necessary to orthogonally command the color xy parameter, dimming
844 * parameter, and/or on/off parameter independently. So we add corresponding advanced level 'CHANNEL_2_BLAH_ONLY'
845 * channels for that purpose. Since they are advanced level, normal users should normally not be confused by them,
846 * yet advanced users can use them nevertheless.
848 private void updateChannelList() {
850 synchronized (supportedChannelIdSet) {
851 logger.debug("{} -> updateChannelList()", resourceId);
853 if (supportedChannelIdSet.contains(CHANNEL_2_COLOR)) {
854 supportedChannelIdSet.add(CHANNEL_2_COLOR_XY_ONLY);
856 supportedChannelIdSet.remove(CHANNEL_2_BRIGHTNESS);
857 supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
859 supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
860 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
862 if (supportedChannelIdSet.contains(CHANNEL_2_BRIGHTNESS)) {
863 supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
865 supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
866 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
868 if (supportedChannelIdSet.contains(CHANNEL_2_SWITCH)) {
869 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
873 * This binding creates its dynamic list of channels by a 'subtractive' method i.e. the full set of
874 * channels is initially created from the thing type xml, and then for any channels where UndfType.NULL
875 * data is returned, the respective channel is removed from the full list. However in seldom cases
876 * UndfType.NULL may wrongly be returned, so we should log a warning here just in case.
878 if (logger.isDebugEnabled()) {
879 supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
880 .forEach(channelId -> logger.debug(
881 "{} -> updateChannelList() required channel '{}' missing", resourceId, channelId));
883 supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
884 .forEach(channelId -> logger.warn(
885 "Thing '{}' is missing required channel '{}'. Please recreate the thing!",
886 thing.getUID(), channelId));
889 // get list of unused channels
890 List<Channel> unusedChannels = thing.getChannels().stream()
891 .filter(channel -> !supportedChannelIdSet.contains(channel.getUID().getId()))
892 .collect(Collectors.toList());
894 // remove any unused channels
895 if (!unusedChannels.isEmpty()) {
896 if (logger.isDebugEnabled()) {
897 unusedChannels.stream().map(channel -> channel.getUID().getId())
898 .forEach(channelId -> logger.debug(
899 "{} -> updateChannelList() removing unused channel '{}'", resourceId,
902 updateThing(editThing().withoutChannels(unusedChannels).build());
909 * Update the state of the existing channels.
911 * @param resource the Resource containing the new channel state.
912 * @return true if the channel was found and updated.
914 private boolean updateChannels(Resource resource) {
915 logger.debug("{} -> updateChannels() from resource {}", resourceId, resource);
916 boolean fullUpdate = resource.hasFullState();
917 switch (resource.getType()) {
920 addSupportedChannel(CHANNEL_2_BUTTON_LAST_EVENT);
921 addSupportedChannel(CHANNEL_2_BUTTON_LAST_UPDATED);
922 controlIds.put(resource.getId(), resource.getControlId());
924 State buttonState = resource.getButtonEventState(controlIds);
925 updateState(CHANNEL_2_BUTTON_LAST_EVENT, buttonState, fullUpdate);
927 // Update channel from timestamp if last button pressed.
928 State buttonLastUpdatedState = resource.getButtonLastUpdatedState(timeZoneProvider.getTimeZone());
929 if (buttonLastUpdatedState instanceof DateTimeType) {
930 Instant buttonLastUpdatedInstant = ((DateTimeType) buttonLastUpdatedState).getInstant();
931 if (buttonLastUpdatedInstant.isAfter(buttonGroupLastUpdated)) {
932 updateState(CHANNEL_2_BUTTON_LAST_UPDATED, buttonLastUpdatedState, fullUpdate);
933 buttonGroupLastUpdated = buttonLastUpdatedInstant;
935 } else if (Instant.MIN.equals(buttonGroupLastUpdated)) {
936 updateState(CHANNEL_2_BUTTON_LAST_UPDATED, buttonLastUpdatedState, fullUpdate);
941 updateState(CHANNEL_2_BATTERY_LEVEL, resource.getBatteryLevelState(), fullUpdate);
942 updateState(CHANNEL_2_BATTERY_LOW, resource.getBatteryLowState(), fullUpdate);
947 updateEffectChannel(resource);
949 updateState(CHANNEL_2_COLOR_TEMP_PERCENT, resource.getColorTemperaturePercentState(), fullUpdate);
950 updateState(CHANNEL_2_COLOR_TEMP_ABSOLUTE, resource.getColorTemperatureAbsoluteState(), fullUpdate);
951 updateState(CHANNEL_2_COLOR, resource.getColorState(), fullUpdate);
952 updateState(CHANNEL_2_COLOR_XY_ONLY, resource.getColorXyState(), fullUpdate);
953 updateState(CHANNEL_2_EFFECT, resource.getEffectState(), fullUpdate);
954 // fall through for dimming and on/off related channels
958 updateAlertChannel(resource);
960 updateState(CHANNEL_2_BRIGHTNESS, resource.getBrightnessState(), fullUpdate);
961 updateState(CHANNEL_2_DIMMING_ONLY, resource.getDimmingState(), fullUpdate);
962 updateState(CHANNEL_2_SWITCH, resource.getOnOffState(), fullUpdate);
963 updateState(CHANNEL_2_ON_OFF_ONLY, resource.getOnOffState(), fullUpdate);
964 updateState(CHANNEL_2_ALERT, resource.getAlertState(), fullUpdate);
968 updateState(CHANNEL_2_LIGHT_LEVEL, resource.getLightLevelState(), fullUpdate);
969 updateState(CHANNEL_2_LIGHT_LEVEL_LAST_UPDATED,
970 resource.getLightLevelLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
971 updateState(CHANNEL_2_LIGHT_LEVEL_ENABLED, resource.getEnabledState(), fullUpdate);
976 updateState(CHANNEL_2_MOTION, resource.getMotionState(), fullUpdate);
977 updateState(CHANNEL_2_MOTION_LAST_UPDATED,
978 resource.getMotionLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
979 updateState(CHANNEL_2_MOTION_ENABLED, resource.getEnabledState(), fullUpdate);
982 case RELATIVE_ROTARY:
984 addSupportedChannel(CHANNEL_2_ROTARY_STEPS);
985 addSupportedChannel(CHANNEL_2_ROTARY_STEPS_LAST_UPDATED);
987 updateState(CHANNEL_2_ROTARY_STEPS, resource.getRotaryStepsState(), fullUpdate);
989 updateState(CHANNEL_2_ROTARY_STEPS_LAST_UPDATED,
990 resource.getRotaryStepsLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
994 updateState(CHANNEL_2_TEMPERATURE, resource.getTemperatureState(), fullUpdate);
995 updateState(CHANNEL_2_TEMPERATURE_LAST_UPDATED,
996 resource.getTemperatureLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
997 updateState(CHANNEL_2_TEMPERATURE_ENABLED, resource.getEnabledState(), fullUpdate);
1000 case ZIGBEE_CONNECTIVITY:
1001 updateConnectivityState(resource);
1005 updateState(CHANNEL_2_SCENE, resource.getSceneState(), fullUpdate);
1009 updateState(CHANNEL_2_SECURITY_CONTACT, resource.getContactState(), fullUpdate);
1010 updateState(CHANNEL_2_SECURITY_CONTACT_LAST_UPDATED,
1011 resource.getContactLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
1012 updateState(CHANNEL_2_SECURITY_CONTACT_ENABLED, resource.getEnabledState(), fullUpdate);
1016 updateState(CHANNEL_2_SECURITY_TAMPER, resource.getTamperState(), fullUpdate);
1017 updateState(CHANNEL_2_SECURITY_TAMPER_LAST_UPDATED,
1018 resource.getTamperLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
1022 updateState(CHANNEL_2_SCENE, resource.getSmartSceneState(), fullUpdate);
1028 if (thisResource.getType() == ResourceType.DEVICE) {
1029 updateState(CHANNEL_2_LAST_UPDATED, new DateTimeType(), fullUpdate);
1035 * Check the Zigbee connectivity and set the thing online status accordingly. If the thing is offline then set all
1036 * its channel states to undefined, otherwise execute a refresh command to update channels to the latest current
1039 * @param resource a Resource that potentially contains the Zigbee connectivity state.
1041 private void updateConnectivityState(Resource resource) {
1042 ZigbeeStatus zigbeeStatus = resource.getZigbeeStatus();
1043 if (Objects.nonNull(zigbeeStatus)) {
1044 logger.debug("{} -> updateConnectivityState() thingStatus:{}, zigbeeStatus:{}", resourceId,
1045 thing.getStatus(), zigbeeStatus);
1046 hasConnectivityIssue = zigbeeStatus != ZigbeeStatus.CONNECTED;
1047 if (hasConnectivityIssue) {
1048 if (thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.COMMUNICATION_ERROR) {
1049 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
1050 "@text/offline.api2.comm-error.zigbee-connectivity-issue");
1051 supportedChannelIdSet.forEach(channelId -> updateState(channelId, UnDefType.UNDEF));
1053 } else if (thing.getStatus() != ThingStatus.ONLINE) {
1054 updateStatus(ThingStatus.ONLINE);
1055 refreshAllChannels();
1061 * Get all resources needed for building the thing state. Build the forward / reverse contributor lookup maps. Set
1062 * up the final list of channels in the thing.
1064 private synchronized void updateDependencies() {
1065 if (!disposing && !updateDependenciesDone) {
1066 logger.debug("{} -> updateDependencies()", resourceId);
1068 if (!updatePropertiesDone) {
1069 logger.debug("{} -> updateDependencies() properties not initialized", resourceId);
1072 if (!updateSceneContributorsDone && !updateSceneContributors()) {
1073 logger.debug("{} -> updateDependencies() scenes not initialized", resourceId);
1077 updateServiceContributors();
1078 updateChannelList();
1079 updateChannelItemLinksFromLegacy();
1080 if (!hasConnectivityIssue) {
1081 updateStatus(ThingStatus.ONLINE);
1083 updateDependenciesDone = true;
1084 } catch (ApiException e) {
1085 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
1086 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
1087 } catch (AssetNotLoadedException e) {
1088 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
1089 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
1090 "@text/offline.api2.conf-error.assets-not-loaded");
1091 } catch (InterruptedException e) {
1097 * Process the incoming Resource to initialize the fixed resp. timed effects channel.
1099 * @param resource a Resource possibly containing a fixed and/or timed effects element.
1101 public void updateEffectChannel(Resource resource) {
1102 Effects fixedEffects = resource.getFixedEffects();
1103 TimedEffects timedEffects = resource.getTimedEffects();
1104 List<StateOption> stateOptions = Stream
1105 .concat(Objects.nonNull(fixedEffects) ? fixedEffects.getStatusValues().stream() : Stream.empty(),
1106 Objects.nonNull(timedEffects) ? timedEffects.getStatusValues().stream() : Stream.empty())
1108 String effectName = EffectType.of(effect).name();
1109 return new StateOption(effectName, effectName);
1110 }).distinct().collect(Collectors.toList());
1111 if (!stateOptions.isEmpty()) {
1112 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT), stateOptions);
1113 logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size());
1118 * Update the light properties.
1120 * @param resource a Resource object containing the property data.
1122 private synchronized void updateLightProperties(Resource resource) {
1123 if (!disposing && !updateLightPropertiesDone) {
1124 logger.debug("{} -> updateLightProperties()", resourceId);
1126 Dimming dimming = resource.getDimming();
1127 thing.setProperty(PROPERTY_DIMMING_RANGE, Objects.nonNull(dimming) ? dimming.toPropertyValue() : null);
1129 MirekSchema mirekSchema = resource.getMirekSchema();
1130 thing.setProperty(PROPERTY_COLOR_TEMP_RANGE,
1131 Objects.nonNull(mirekSchema) ? mirekSchema.toPropertyValue() : null);
1133 ColorXy colorXy = resource.getColorXy();
1134 Gamut2 gamut = Objects.nonNull(colorXy) ? colorXy.getGamut2() : null;
1135 thing.setProperty(PROPERTY_COLOR_GAMUT, Objects.nonNull(gamut) ? gamut.toPropertyValue() : null);
1137 updateLightPropertiesDone = true;
1142 * Initialize the lookup maps of resources that contribute to the thing state.
1144 private void updateLookups() {
1146 logger.debug("{} -> updateLookups()", resourceId);
1147 // get supported services
1148 List<ResourceReference> services = thisResource.getServiceReferences();
1150 // add supported services to contributorsCache
1151 serviceContributorsCache.clear();
1152 serviceContributorsCache.putAll(services.stream()
1153 .collect(Collectors.toMap(ResourceReference::getId, r -> new Resource(r.getType()))));
1155 // add supported services to commandResourceIds
1156 commandResourceIds.clear();
1157 commandResourceIds.putAll(services.stream() // use a 'mergeFunction' to prevent duplicates
1158 .collect(Collectors.toMap(ResourceReference::getType, ResourceReference::getId, (r1, r2) -> r1)));
1163 * Update the primary device properties.
1165 * @param resource a Resource object containing the property data.
1167 private synchronized void updateProperties(Resource resource) {
1168 if (!disposing && !updatePropertiesDone) {
1169 logger.debug("{} -> updateProperties()", resourceId);
1170 Map<String, String> properties = new HashMap<>(thing.getProperties());
1173 properties.put(PROPERTY_RESOURCE_TYPE, thisResource.getType().toString());
1174 properties.put(PROPERTY_RESOURCE_NAME, thisResource.getName());
1176 // owner information
1177 ResourceReference owner = thisResource.getOwner();
1178 if (Objects.nonNull(owner)) {
1179 String ownerId = owner.getId();
1180 if (Objects.nonNull(ownerId)) {
1181 properties.put(PROPERTY_OWNER, ownerId);
1183 ResourceType ownerType = owner.getType();
1184 properties.put(PROPERTY_OWNER_TYPE, ownerType.toString());
1188 MetaData metaData = thisResource.getMetaData();
1189 if (Objects.nonNull(metaData)) {
1190 properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString());
1194 ProductData productData = thisResource.getProductData();
1195 if (Objects.nonNull(productData)) {
1196 String modelId = productData.getModelId();
1198 // standard properties
1199 properties.put(PROPERTY_RESOURCE_ID, resourceId);
1200 properties.put(Thing.PROPERTY_MODEL_ID, modelId);
1201 properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName());
1202 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion());
1203 String hardwarePlatformType = productData.getHardwarePlatformType();
1204 if (Objects.nonNull(hardwarePlatformType)) {
1205 properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType);
1208 // hue specific properties
1209 properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName());
1210 properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString());
1211 properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString());
1213 // Check device for needed work-arounds.
1214 if (LK_WISER_DIMMER_MODEL_ID.equals(modelId)) {
1215 // Apply transition time as a workaround for LK Wiser Dimmer firmware bug.
1216 // Additional details here: https://techblog.vindvejr.dk/?p=455
1217 applyOffTransitionWorkaround = true;
1218 logger.debug("{} -> enabling work-around for turning off LK Wiser Dimmer", resourceId);
1222 thing.setProperties(properties);
1223 updatePropertiesDone = true;
1228 * Execute an HTTP GET command to fetch the resources data for the referenced resource.
1230 * @param reference to the required resource.
1231 * @throws ApiException if a communication error occurred.
1232 * @throws AssetNotLoadedException if one of the assets is not loaded.
1233 * @throws InterruptedException
1235 private void updateResource(ResourceReference reference)
1236 throws ApiException, AssetNotLoadedException, InterruptedException {
1238 logger.debug("{} -> updateResource() from resource {}", resourceId, reference);
1239 getBridgeHandler().getResources(reference).getResources().stream()
1240 .forEach(resource -> onResource(resource));
1245 * Update the scene channel state description selection options
1247 private void updateSceneChannelStateDescription() {
1248 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_SCENE),
1249 sceneResourceEntries.keySet().stream().map(n -> new StateOption(n, n)).collect(Collectors.toList()));
1253 * Fetch the full list of normal resp. smart scenes from the bridge, and call
1254 * {@code updateSceneContributors(List<Resource> allScenes)}
1256 * @throws ApiException if a communication error occurred.
1257 * @throws AssetNotLoadedException if one of the assets is not loaded.
1258 * @throws InterruptedException
1260 public boolean updateSceneContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1261 if (!disposing && !updateSceneContributorsDone) {
1262 List<Resource> allScenes = new ArrayList<>();
1263 for (ResourceType type : SUPPORTED_SCENE_TYPES) {
1264 allScenes.addAll(getBridgeHandler().getResources(new ResourceReference().setType(type)).getResources());
1266 updateSceneContributors(allScenes);
1268 return updateSceneContributorsDone;
1272 * Process the incoming list of normal resp. smart scene resources to find those which contribute to this thing. And
1273 * if there are any, include a scene channel in the supported channel list, and populate its respective state
1276 * @param allScenes the full list of normal resp. smart scene resources.
1278 public synchronized boolean updateSceneContributors(List<Resource> allScenes) {
1279 if (!disposing && !updateSceneContributorsDone) {
1280 sceneContributorsCache.clear();
1281 sceneResourceEntries.clear();
1283 ResourceReference thisReference = getResourceReference();
1284 Set<Resource> scenes = allScenes.stream().filter(s -> thisReference.equals(s.getGroup()))
1285 .collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Resource::getName))));
1287 if (!scenes.isEmpty()) {
1288 sceneContributorsCache.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)));
1289 sceneResourceEntries.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s)));
1291 State state = Objects.requireNonNull(
1292 scenes.stream().filter(s -> Objects.requireNonNullElse(s.getSceneActive(), false))
1293 .map(s -> s.getSceneState()).findAny().orElse(UnDefType.UNDEF));
1295 // create scene channel if it is missing
1296 if (getThing().getChannel(CHANNEL_2_SCENE) == null) {
1297 updateThing(editThing()
1298 .withChannel(ChannelBuilder.create(new ChannelUID(getThing().getUID(), CHANNEL_2_SCENE))
1299 .withType(new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_2_SCENE)).build())
1303 updateState(CHANNEL_2_SCENE, state, true);
1304 updateSceneChannelStateDescription();
1306 logger.debug("{} -> updateSceneContributors() found {} normal resp. smart scenes", resourceId,
1309 updateSceneContributorsDone = true;
1311 return updateSceneContributorsDone;
1315 * Execute a series of HTTP GET commands to fetch the resource data for all service resources that contribute to the
1318 * @throws ApiException if a communication error occurred.
1319 * @throws AssetNotLoadedException if one of the assets is not loaded.
1320 * @throws InterruptedException
1322 private void updateServiceContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1324 logger.debug("{} -> updateServiceContributors() called for {} contributors", resourceId,
1325 serviceContributorsCache.size());
1326 ResourceReference reference = new ResourceReference();
1327 for (var entry : serviceContributorsCache.entrySet()) {
1328 updateResource(reference.setId(entry.getKey()).setType(entry.getValue().getType()));
1334 * Update the channel state, and if appropriate add the channel ID to the set of supportedChannelIds. Calls either
1335 * OH core updateState() or triggerChannel() methods depending on the channel kind.
1337 * Note: the particular 'UnDefType.UNDEF' value of the state argument is used to specially indicate the undefined
1338 * state, but yet that its channel shall nevertheless continue to be present in the thing.
1340 * @param channelID the id of the channel.
1341 * @param state the new state of the channel.
1342 * @param fullUpdate if true always update the channel, otherwise only update if state is not 'UNDEF'.
1344 private void updateState(String channelID, State state, boolean fullUpdate) {
1345 boolean isDefined = state != UnDefType.NULL;
1346 Channel channel = thing.getChannel(channelID);
1348 if ((fullUpdate || isDefined) && Objects.nonNull(channel)) {
1349 logger.debug("{} -> updateState() '{}' update with '{}' (fullUpdate:{}, isDefined:{})", resourceId,
1350 channelID, state, fullUpdate, isDefined);
1352 switch (channel.getKind()) {
1354 updateState(channelID, state);
1358 if (state instanceof DecimalType) {
1359 triggerChannel(channelID, String.valueOf(((DecimalType) state).intValue()));
1363 if (fullUpdate && isDefined) {
1364 addSupportedChannel(channelID);
1369 * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's
1370 * settings into this thing.
1372 private void updateThingFromLegacy() {
1373 if (isInitialized()) {
1374 logger.warn("Cannot update thing '{}' from legacy thing since handler already initialized.",
1378 Map<String, String> properties = thing.getProperties();
1379 String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID);
1380 if (Objects.nonNull(legacyThingUID)) {
1381 Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID));
1382 if (Objects.nonNull(legacyThing)) {
1383 ThingBuilder editBuilder = editThing();
1385 String location = legacyThing.getLocation();
1386 if (Objects.nonNull(location) && !location.isBlank()) {
1387 editBuilder = editBuilder.withLocation(location);
1390 // save list of legacyLinkedChannelUIDs for use after channel list is initialised
1391 legacyLinkedChannelUIDs.clear();
1392 legacyLinkedChannelUIDs.addAll(legacyThing.getChannels().stream().map(Channel::getUID)
1393 .filter(uid -> REPLICATE_CHANNEL_ID_MAP.containsKey(uid.getId())
1394 && itemChannelLinkRegistry.isLinked(uid))
1395 .collect(Collectors.toList()));
1397 Map<String, String> newProperties = new HashMap<>(properties);
1398 newProperties.remove(PROPERTY_LEGACY_THING_UID);
1400 updateThing(editBuilder.withProperties(newProperties).build());