2 * Copyright (c) 2010-2023 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.HashMap;
23 import java.util.HashSet;
24 import java.util.List;
26 import java.util.Objects;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.CopyOnWriteArrayList;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.TimeUnit;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.hue.internal.action.DynamicsActions;
38 import org.openhab.binding.hue.internal.config.Clip2ThingConfig;
39 import org.openhab.binding.hue.internal.dto.clip2.Alerts;
40 import org.openhab.binding.hue.internal.dto.clip2.ColorXy;
41 import org.openhab.binding.hue.internal.dto.clip2.Dimming;
42 import org.openhab.binding.hue.internal.dto.clip2.Effects;
43 import org.openhab.binding.hue.internal.dto.clip2.Gamut2;
44 import org.openhab.binding.hue.internal.dto.clip2.MetaData;
45 import org.openhab.binding.hue.internal.dto.clip2.MirekSchema;
46 import org.openhab.binding.hue.internal.dto.clip2.ProductData;
47 import org.openhab.binding.hue.internal.dto.clip2.Resource;
48 import org.openhab.binding.hue.internal.dto.clip2.ResourceReference;
49 import org.openhab.binding.hue.internal.dto.clip2.Resources;
50 import org.openhab.binding.hue.internal.dto.clip2.TimedEffects;
51 import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
52 import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
53 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
54 import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
55 import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
56 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
57 import org.openhab.binding.hue.internal.dto.clip2.helper.Setters;
58 import org.openhab.binding.hue.internal.exceptions.ApiException;
59 import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
60 import org.openhab.core.i18n.TimeZoneProvider;
61 import org.openhab.core.library.types.DateTimeType;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.HSBType;
64 import org.openhab.core.library.types.IncreaseDecreaseType;
65 import org.openhab.core.library.types.OnOffType;
66 import org.openhab.core.library.types.PercentType;
67 import org.openhab.core.library.types.QuantityType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.library.unit.MetricPrefix;
70 import org.openhab.core.library.unit.Units;
71 import org.openhab.core.thing.Bridge;
72 import org.openhab.core.thing.Channel;
73 import org.openhab.core.thing.ChannelUID;
74 import org.openhab.core.thing.Thing;
75 import org.openhab.core.thing.ThingRegistry;
76 import org.openhab.core.thing.ThingStatus;
77 import org.openhab.core.thing.ThingStatusDetail;
78 import org.openhab.core.thing.ThingTypeUID;
79 import org.openhab.core.thing.ThingUID;
80 import org.openhab.core.thing.binding.BaseThingHandler;
81 import org.openhab.core.thing.binding.BridgeHandler;
82 import org.openhab.core.thing.binding.ThingHandlerService;
83 import org.openhab.core.thing.binding.builder.ThingBuilder;
84 import org.openhab.core.thing.link.ItemChannelLink;
85 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
86 import org.openhab.core.types.Command;
87 import org.openhab.core.types.RefreshType;
88 import org.openhab.core.types.State;
89 import org.openhab.core.types.StateOption;
90 import org.openhab.core.types.UnDefType;
91 import org.slf4j.Logger;
92 import org.slf4j.LoggerFactory;
95 * Handler for things based on CLIP 2 'device', 'room', or 'zone resources.
97 * @author Andrew Fiddian-Green - Initial contribution.
100 public class Clip2ThingHandler extends BaseThingHandler {
102 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM,
105 private static final Set<ResourceType> SUPPORTED_SCENE_TYPES = Set.of(ResourceType.SCENE, ResourceType.SMART_SCENE);
107 private static final Duration DYNAMICS_ACTIVE_WINDOW = Duration.ofSeconds(10);
109 private static final String LK_WISER_DIMMER_MODEL_ID = "LK Dimmer";
111 private final Logger logger = LoggerFactory.getLogger(Clip2ThingHandler.class);
114 * A map of service Resources whose state contributes to the overall state of this thing. It is a map between the
115 * resource ID (string) and a Resource object containing the last known state. e.g. a DEVICE thing may support a
116 * LIGHT service whose Resource contributes to its overall state, or a ROOM or ZONE thing may support a
117 * GROUPED_LIGHT service whose Resource contributes to the its overall state.
119 private final Map<String, Resource> serviceContributorsCache = new ConcurrentHashMap<>();
122 * A map of Resource IDs which are targets for commands to be sent. It is a map between the type of command
123 * (ResourcesType) and the resource ID to which the command shall be sent. e.g. a LIGHT 'on' command shall be sent
124 * to the respective LIGHT resource ID.
126 private final Map<ResourceType, String> commandResourceIds = new ConcurrentHashMap<>();
129 * Button devices contain one or more physical buttons, each of which is represented by a BUTTON Resource with its
130 * own unique resource ID, and a respective controlId that indicates which button it is in the device. e.g. a dimmer
131 * pad has four buttons (controlId's 1..4) each represented by a BUTTON Resource with a unique resource ID. This is
132 * a map between the resource ID and its respective controlId.
134 private final Map<String, Integer> controlIds = new ConcurrentHashMap<>();
137 * The set of channel IDs that are supported by this thing. e.g. an on/off light may support 'switch' and
138 * 'zigbeeStatus' channels, whereas a complex light may support 'switch', 'brightness', 'color', 'color temperature'
139 * and 'zigbeeStatus' channels.
141 private final Set<String> supportedChannelIdSet = new HashSet<>();
144 * A map of scene IDs versus scene Resources for the scenes that contribute to and command this thing. It is a map
145 * between the resource ID (string) and a Resource object containing the scene's last known state.
147 private final Map<String, Resource> sceneContributorsCache = new ConcurrentHashMap<>();
150 * A map of scene names versus scene Resources for the scenes that contribute to and command this thing. e.g. a
151 * command for a scene named 'Energize' shall be sent to the respective SCENE resource ID.
153 private final Map<String, Resource> sceneResourceEntries = new ConcurrentHashMap<>();
156 * A list of API v1 thing channel UIDs that are linked to items. It is used in the process of replicating the
157 * Item/Channel links from a legacy v1 thing to this API v2 thing.
159 private final List<ChannelUID> legacyLinkedChannelUIDs = new CopyOnWriteArrayList<>();
161 private final ThingRegistry thingRegistry;
162 private final ItemChannelLinkRegistry itemChannelLinkRegistry;
163 private final Clip2StateDescriptionProvider stateDescriptionProvider;
164 private final TimeZoneProvider timeZoneProvider;
166 private String resourceId = "?";
167 private Resource thisResource;
168 private Duration dynamicsDuration = Duration.ZERO;
169 private Instant dynamicsExpireTime = Instant.MIN;
170 private Instant buttonGroupLastUpdated = Instant.MIN;
172 private boolean disposing;
173 private boolean hasConnectivityIssue;
174 private boolean updateSceneContributorsDone;
175 private boolean updateLightPropertiesDone;
176 private boolean updatePropertiesDone;
177 private boolean updateDependenciesDone;
178 private boolean applyOffTransitionWorkaround;
180 private @Nullable Future<?> alertResetTask;
181 private @Nullable Future<?> dynamicsResetTask;
182 private @Nullable Future<?> updateDependenciesTask;
183 private @Nullable Future<?> updateServiceContributorsTask;
185 public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescriptionProvider,
186 TimeZoneProvider timeZoneProvider, ThingRegistry thingRegistry,
187 ItemChannelLinkRegistry itemChannelLinkRegistry) {
190 ThingTypeUID thingTypeUID = thing.getThingTypeUID();
191 if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
192 thisResource = new Resource(ResourceType.DEVICE);
193 } else if (THING_TYPE_ROOM.equals(thingTypeUID)) {
194 thisResource = new Resource(ResourceType.ROOM);
195 } else if (THING_TYPE_ZONE.equals(thingTypeUID)) {
196 thisResource = new Resource(ResourceType.ZONE);
198 throw new IllegalArgumentException("Wrong thing type " + thingTypeUID.getAsString());
201 this.thingRegistry = thingRegistry;
202 this.itemChannelLinkRegistry = itemChannelLinkRegistry;
203 this.stateDescriptionProvider = stateDescriptionProvider;
204 this.timeZoneProvider = timeZoneProvider;
208 * Add a channel ID to the supportedChannelIdSet set. If the channel supports dynamics (timed transitions) then add
209 * the respective channel as well.
211 * @param channelId the channel ID to add.
213 private void addSupportedChannel(String channelId) {
214 if (!disposing && !updateDependenciesDone) {
215 synchronized (supportedChannelIdSet) {
216 logger.debug("{} -> addSupportedChannel() '{}' added to supported channel set", resourceId, channelId);
217 supportedChannelIdSet.add(channelId);
218 if (DYNAMIC_CHANNELS.contains(channelId)) {
219 clearDynamicsChannel();
226 * Cancel the given task.
228 * @param cancelTask the task to be cancelled (may be null)
229 * @param mayInterrupt allows cancel() to interrupt the thread.
231 private void cancelTask(@Nullable Future<?> cancelTask, boolean mayInterrupt) {
232 if (Objects.nonNull(cancelTask)) {
233 cancelTask.cancel(mayInterrupt);
238 * Clear the dynamics channel parameters.
240 private void clearDynamicsChannel() {
241 dynamicsExpireTime = Instant.MIN;
242 dynamicsDuration = Duration.ZERO;
243 updateState(CHANNEL_2_DYNAMICS, new QuantityType<>(0, MetricPrefix.MILLI(Units.SECOND)), true);
247 public void dispose() {
248 logger.debug("{} -> dispose()", resourceId);
250 cancelTask(alertResetTask, true);
251 cancelTask(dynamicsResetTask, true);
252 cancelTask(updateDependenciesTask, true);
253 cancelTask(updateServiceContributorsTask, true);
254 alertResetTask = null;
255 dynamicsResetTask = null;
256 updateDependenciesTask = null;
257 updateServiceContributorsTask = null;
258 legacyLinkedChannelUIDs.clear();
259 sceneContributorsCache.clear();
260 sceneResourceEntries.clear();
261 supportedChannelIdSet.clear();
262 commandResourceIds.clear();
263 serviceContributorsCache.clear();
268 * Get the bridge handler.
270 * @throws AssetNotLoadedException if the handler does not exist.
272 private Clip2BridgeHandler getBridgeHandler() throws AssetNotLoadedException {
273 Bridge bridge = getBridge();
274 if (Objects.nonNull(bridge)) {
275 BridgeHandler handler = bridge.getHandler();
276 if (handler instanceof Clip2BridgeHandler) {
277 return (Clip2BridgeHandler) handler;
280 throw new AssetNotLoadedException("Bridge handler missing");
284 * Do a double lookup to get the cached resource that matches the given ResourceType.
286 * @param resourceType the type to search for.
287 * @return the Resource, or null if not found.
289 private @Nullable Resource getCachedResource(ResourceType resourceType) {
290 String commandResourceId = commandResourceIds.get(resourceType);
291 return Objects.nonNull(commandResourceId) ? serviceContributorsCache.get(commandResourceId) : null;
295 * Return a ResourceReference to this handler's resource.
297 * @return a ResourceReference instance.
299 public ResourceReference getResourceReference() {
300 return new ResourceReference().setId(resourceId).setType(thisResource.getType());
304 * Register the 'DynamicsAction' service.
307 public Collection<Class<? extends ThingHandlerService>> getServices() {
308 return Set.of(DynamicsActions.class);
312 public void handleCommand(ChannelUID channelUID, Command commandParam) {
313 if (RefreshType.REFRESH.equals(commandParam)) {
314 if (thing.getStatus() == ThingStatus.ONLINE) {
315 refreshAllChannels();
320 Channel channel = thing.getChannel(channelUID);
321 if (channel == null) {
322 if (logger.isDebugEnabled()) {
323 logger.debug("{} -> handleCommand() channelUID:{} does not exist", resourceId, channelUID);
326 logger.warn("Command received for channel '{}' which is not in thing '{}'.", channelUID,
332 ResourceType lightResourceType = thisResource.getType() == ResourceType.DEVICE ? ResourceType.LIGHT
333 : ResourceType.GROUPED_LIGHT;
335 Resource putResource = null;
336 String putResourceId = null;
337 Command command = commandParam;
338 String channelId = channelUID.getId();
339 Resource cache = getCachedResource(lightResourceType);
342 case CHANNEL_2_ALERT:
343 putResource = Setters.setAlert(new Resource(lightResourceType), command, cache);
344 cancelTask(alertResetTask, false);
345 alertResetTask = scheduler.schedule(
346 () -> updateState(channelUID, new StringType(ActionType.NO_ACTION.name())), 10,
350 case CHANNEL_2_EFFECT:
351 putResource = Setters.setEffect(new Resource(lightResourceType), command, cache).setOnOff(OnOffType.ON);
354 case CHANNEL_2_COLOR_TEMP_PERCENT:
355 if (command instanceof IncreaseDecreaseType) {
356 if (Objects.nonNull(cache)) {
357 State current = cache.getColorTemperaturePercentState();
358 if (current instanceof PercentType) {
359 int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1;
360 int percent = ((PercentType) current).intValue() + (sign * (int) Resource.PERCENT_DELTA);
361 command = new PercentType(Math.min(100, Math.max(0, percent)));
364 } else if (command instanceof OnOffType) {
365 command = OnOffType.OFF == command ? PercentType.ZERO : PercentType.HUNDRED;
367 putResource = Setters.setColorTemperaturePercent(new Resource(lightResourceType), command, cache);
370 case CHANNEL_2_COLOR_TEMP_ABSOLUTE:
371 putResource = Setters.setColorTemperatureAbsolute(new Resource(lightResourceType), command, cache);
374 case CHANNEL_2_COLOR:
375 putResource = new Resource(lightResourceType);
376 if (command instanceof HSBType) {
377 HSBType color = ((HSBType) command);
378 putResource = Setters.setColorXy(putResource, color, cache);
379 command = color.getBrightness();
381 // NB fall through for handling of brightness and switch related commands !!
383 case CHANNEL_2_BRIGHTNESS:
384 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
385 if (command instanceof IncreaseDecreaseType) {
386 if (Objects.nonNull(cache)) {
387 State current = cache.getBrightnessState();
388 if (current instanceof PercentType) {
389 int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1;
390 double percent = ((PercentType) current).doubleValue() + (sign * Resource.PERCENT_DELTA);
391 command = new PercentType(new BigDecimal(Math.min(100f, Math.max(0f, percent)),
392 Resource.PERCENT_MATH_CONTEXT));
396 if (command instanceof PercentType) {
397 PercentType brightness = (PercentType) command;
398 putResource = Setters.setDimming(putResource, brightness, cache);
399 Double minDimLevel = Objects.nonNull(cache) ? cache.getMinimumDimmingLevel() : null;
400 minDimLevel = Objects.nonNull(minDimLevel) ? minDimLevel : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL;
401 command = OnOffType.from(brightness.doubleValue() >= minDimLevel);
403 // NB fall through for handling of switch related commands !!
405 case CHANNEL_2_SWITCH:
406 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
407 putResource.setOnOff(command);
408 applyDeviceSpecificWorkArounds(command, putResource);
411 case CHANNEL_2_COLOR_XY_ONLY:
412 putResource = Setters.setColorXy(new Resource(lightResourceType), command, cache);
415 case CHANNEL_2_DIMMING_ONLY:
416 putResource = Setters.setDimming(new Resource(lightResourceType), command, cache);
419 case CHANNEL_2_ON_OFF_ONLY:
420 putResource = new Resource(lightResourceType).setOnOff(command);
421 applyDeviceSpecificWorkArounds(command, putResource);
424 case CHANNEL_2_TEMPERATURE_ENABLED:
425 putResource = new Resource(ResourceType.TEMPERATURE).setEnabled(command);
428 case CHANNEL_2_MOTION_ENABLED:
429 putResource = new Resource(ResourceType.MOTION).setEnabled(command);
432 case CHANNEL_2_LIGHT_LEVEL_ENABLED:
433 putResource = new Resource(ResourceType.LIGHT_LEVEL).setEnabled(command);
436 case CHANNEL_2_SCENE:
437 if (command instanceof StringType) {
438 Resource scene = sceneResourceEntries.get(((StringType) command).toString());
439 if (Objects.nonNull(scene)) {
440 ResourceType putResourceType = scene.getType();
441 putResource = new Resource(putResourceType);
442 switch (putResourceType) {
444 putResource.setRecallAction(SceneRecallAction.ACTIVE);
447 putResource.setRecallAction(SmartSceneRecallAction.ACTIVATE);
450 logger.debug("{} -> handleCommand() type '{}' is not a supported scene type",
451 resourceId, putResourceType);
454 putResourceId = scene.getId();
459 case CHANNEL_2_DYNAMICS:
460 Duration clearAfter = Duration.ZERO;
461 if (command instanceof QuantityType<?>) {
462 QuantityType<?> durationMs = ((QuantityType<?>) command).toUnit(MetricPrefix.MILLI(Units.SECOND));
463 if (Objects.nonNull(durationMs) && durationMs.longValue() > 0) {
464 Duration duration = Duration.ofMillis(durationMs.longValue());
465 dynamicsDuration = duration;
466 dynamicsExpireTime = Instant.now().plus(DYNAMICS_ACTIVE_WINDOW);
467 clearAfter = DYNAMICS_ACTIVE_WINDOW;
468 logger.debug("{} -> handleCommand() dynamics setting {} valid for {}", resourceId, duration,
472 cancelTask(dynamicsResetTask, false);
473 dynamicsResetTask = scheduler.schedule(() -> clearDynamicsChannel(), clearAfter.toMillis(),
474 TimeUnit.MILLISECONDS);
478 if (logger.isDebugEnabled()) {
479 logger.debug("{} -> handleCommand() channelUID:{} unknown", resourceId, channelUID);
481 logger.warn("Command received for unknown channel '{}'.", channelUID);
486 if (putResource == null) {
487 if (logger.isDebugEnabled()) {
488 logger.debug("{} -> handleCommand() command:{} not supported on channelUID:{}", resourceId, command,
491 logger.warn("Command '{}' is not supported on channel '{}'.", command, channelUID);
496 putResourceId = Objects.nonNull(putResourceId) ? putResourceId : commandResourceIds.get(putResource.getType());
497 if (putResourceId == null) {
498 if (logger.isDebugEnabled()) {
500 "{} -> handleCommand() channelUID:{}, command:{}, putResourceType:{} => missing resource ID",
501 resourceId, channelUID, command, putResource.getType());
503 logger.warn("Command '{}' for channel '{}' cannot be processed by thing '{}'.", command, channelUID,
509 if (DYNAMIC_CHANNELS.contains(channelId)) {
510 if (Instant.now().isBefore(dynamicsExpireTime) && !dynamicsDuration.isZero()
511 && !dynamicsDuration.isNegative()) {
512 if (ResourceType.SCENE == putResource.getType()) {
513 putResource.setRecallDuration(dynamicsDuration);
514 } else if (CHANNEL_2_EFFECT == channelId) {
515 putResource.setTimedEffectsDuration(dynamicsDuration);
517 putResource.setDynamicsDuration(dynamicsDuration);
522 putResource.setId(putResourceId);
523 logger.debug("{} -> handleCommand() put resource {}", resourceId, putResource);
526 Resources resources = getBridgeHandler().putResource(putResource);
527 if (resources.hasErrors()) {
528 logger.info("Command '{}' for thing '{}', channel '{}' succeeded with errors: {}", command,
529 thing.getUID(), channelUID, String.join("; ", resources.getErrors()));
531 } catch (ApiException | AssetNotLoadedException e) {
532 if (logger.isDebugEnabled()) {
533 logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
535 logger.warn("Command '{}' for thing '{}', channel '{}' failed with error '{}'.", command,
536 thing.getUID(), channelUID, e.getMessage());
538 } catch (InterruptedException e) {
542 private void refreshAllChannels() {
543 if (!updateDependenciesDone) {
546 cancelTask(updateServiceContributorsTask, false);
547 updateServiceContributorsTask = scheduler.schedule(() -> {
549 updateServiceContributors();
550 } catch (ApiException | AssetNotLoadedException e) {
551 logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
552 } catch (InterruptedException e) {
554 }, 3, TimeUnit.SECONDS);
558 * Apply device specific work-arounds needed for given command.
560 * @param command the handled command.
561 * @param putResource the resource that will be adjusted if needed.
563 private void applyDeviceSpecificWorkArounds(Command command, Resource putResource) {
564 if (command == OnOffType.OFF && applyOffTransitionWorkaround) {
565 putResource.setDynamicsDuration(dynamicsDuration);
570 * Handle a 'dynamics' command for the given channel ID for the given dynamics duration.
572 * @param channelId the ID of the target channel.
573 * @param command the new target state.
574 * @param duration the transition duration.
576 public synchronized void handleDynamicsCommand(String channelId, Command command, QuantityType<?> duration) {
577 if (DYNAMIC_CHANNELS.contains(channelId)) {
578 Channel dynamicsChannel = thing.getChannel(CHANNEL_2_DYNAMICS);
579 Channel targetChannel = thing.getChannel(channelId);
580 if (Objects.nonNull(dynamicsChannel) && Objects.nonNull(targetChannel)) {
581 logger.debug("{} - handleDynamicsCommand() channelId:{}, command:{}, duration:{}", resourceId,
582 channelId, command, duration);
583 handleCommand(dynamicsChannel.getUID(), duration);
584 handleCommand(targetChannel.getUID(), command);
588 logger.warn("Dynamics command '{}' for thing '{}', channel '{}' and duration'{}' failed.", command,
589 thing.getUID(), channelId, duration);
593 public void initialize() {
594 Clip2ThingConfig config = getConfigAs(Clip2ThingConfig.class);
596 String resourceId = config.resourceId;
597 if (resourceId.isBlank()) {
598 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
599 "@text/offline.api2.conf-error.resource-id-bad");
602 thisResource.setId(resourceId);
603 this.resourceId = resourceId;
604 logger.debug("{} -> initialize()", resourceId);
606 updateThingFromLegacy();
607 updateStatus(ThingStatus.UNKNOWN);
609 dynamicsDuration = Duration.ZERO;
610 dynamicsExpireTime = Instant.MIN;
613 hasConnectivityIssue = false;
614 updatePropertiesDone = false;
615 updateDependenciesDone = false;
616 updateLightPropertiesDone = false;
617 updateSceneContributorsDone = false;
619 Bridge bridge = getBridge();
620 if (Objects.nonNull(bridge)) {
621 BridgeHandler bridgeHandler = bridge.getHandler();
622 if (bridgeHandler instanceof Clip2BridgeHandler) {
623 ((Clip2BridgeHandler) bridgeHandler).childInitialized();
629 * Update the channel state depending on a new resource sent from the bridge.
631 * @param resource a Resource object containing the new state.
633 public void onResource(Resource resource) {
635 boolean resourceConsumed = false;
636 String incomingResourceId = resource.getId();
637 if (resourceId.equals(incomingResourceId)) {
638 if (resource.hasFullState()) {
639 thisResource = resource;
640 if (!updatePropertiesDone) {
641 updateProperties(resource);
642 resourceConsumed = updatePropertiesDone;
645 if (!updateDependenciesDone) {
646 resourceConsumed = true;
647 cancelTask(updateDependenciesTask, false);
648 updateDependenciesTask = scheduler.submit(() -> updateDependencies());
650 } else if (SUPPORTED_SCENE_TYPES.contains(resource.getType())) {
651 Resource cachedScene = sceneContributorsCache.get(incomingResourceId);
652 if (Objects.nonNull(cachedScene)) {
653 Setters.setResource(resource, cachedScene);
654 resourceConsumed = updateChannels(resource);
655 sceneContributorsCache.put(incomingResourceId, resource);
658 Resource cachedService = serviceContributorsCache.get(incomingResourceId);
659 if (Objects.nonNull(cachedService)) {
660 Setters.setResource(resource, cachedService);
661 resourceConsumed = updateChannels(resource);
662 serviceContributorsCache.put(incomingResourceId, resource);
663 if (ResourceType.LIGHT == resource.getType() && !updateLightPropertiesDone) {
664 updateLightProperties(resource);
668 if (resourceConsumed) {
669 logger.debug("{} -> onResource() consumed resource {}", resourceId, resource);
675 * Update the thing internal state depending on a full list of resources sent from the bridge. If the resourceType
676 * is SCENE then call updateScenes(), otherwise if the resource refers to this thing, consume it via onResource() as
677 * any other resource, or else if the resourceType nevertheless matches the thing type, set the thing state offline.
679 * @param resourceType the type of the resources in the list.
680 * @param fullResources the full list of resources of the given type.
682 public void onResourcesList(ResourceType resourceType, List<Resource> fullResources) {
683 if (resourceType == ResourceType.SCENE) {
684 updateSceneContributors(fullResources);
686 fullResources.stream().filter(r -> resourceId.equals(r.getId())).findAny()
687 .ifPresentOrElse(r -> onResource(r), () -> {
688 if (resourceType == thisResource.getType()) {
689 logger.debug("{} -> onResourcesList() configuration error: unknown resourceId", resourceId);
690 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
691 "@text/offline.api2.conf-error.resource-id-bad");
698 * Process the incoming Resource to initialize the alert channel.
700 * @param resource a Resource possibly with an Alerts element.
702 private void updateAlertChannel(Resource resource) {
703 Alerts alerts = resource.getAlerts();
704 if (Objects.nonNull(alerts)) {
705 List<StateOption> stateOptions = alerts.getActionValues().stream().map(action -> action.name())
706 .map(actionId -> new StateOption(actionId, actionId)).collect(Collectors.toList());
707 if (!stateOptions.isEmpty()) {
708 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_ALERT), stateOptions);
709 logger.debug("{} -> updateAlerts() found {} associated alerts", resourceId, stateOptions.size());
715 * If this v2 thing has a matching v1 legacy thing in the system, then for each channel in the v1 thing that
716 * corresponds to an equivalent channel in this v2 thing, and for all items that are linked to the v1 channel,
717 * create a new channel/item link between that item and the respective v2 channel in this thing.
719 private void updateChannelItemLinksFromLegacy() {
721 legacyLinkedChannelUIDs.forEach(legacyLinkedChannelUID -> {
722 String targetChannelId = REPLICATE_CHANNEL_ID_MAP.get(legacyLinkedChannelUID.getId());
723 if (Objects.nonNull(targetChannelId)) {
724 Channel targetChannel = thing.getChannel(targetChannelId);
725 if (Objects.nonNull(targetChannel)) {
726 ChannelUID uid = targetChannel.getUID();
727 itemChannelLinkRegistry.getLinkedItems(legacyLinkedChannelUID).forEach(linkedItem -> {
728 String item = linkedItem.getName();
729 if (!itemChannelLinkRegistry.isLinked(item, uid)) {
730 if (logger.isDebugEnabled()) {
732 "{} -> updateChannelItemLinksFromLegacy() item:{} linked to channel:{}",
733 resourceId, item, uid);
735 logger.info("Item '{}' linked to thing '{}' channel '{}'", item, thing.getUID(),
738 itemChannelLinkRegistry.add(new ItemChannelLink(item, uid));
744 legacyLinkedChannelUIDs.clear();
749 * Set the active list of channels by removing any that had initially been created by the thing XML declaration, but
750 * which in fact did not have data returned from the bridge i.e. channels which are not in the supportedChannelIdSet
752 * Also warn if there are channels in the supportedChannelIdSet set which are not in the thing.
754 * Adjusts the channel list so that only the highest level channel is available in the normal channel list. If a
755 * light supports the color channel, then it's brightness and switch can be commanded via the 'B' part of the HSB
756 * channel value. And if it supports the brightness channel the switch can be controlled via the brightness. So we
757 * can remove these lower level channels from the normal channel list.
759 * For more advanced applications, it is necessary to orthogonally command the color xy parameter, dimming
760 * parameter, and/or on/off parameter independently. So we add corresponding advanced level 'CHANNEL_2_BLAH_ONLY'
761 * channels for that purpose. Since they are advanced level, normal users should normally not be confused by them,
762 * yet advanced users can use them nevertheless.
764 private void updateChannelList() {
766 synchronized (supportedChannelIdSet) {
767 logger.debug("{} -> updateChannelList()", resourceId);
769 if (supportedChannelIdSet.contains(CHANNEL_2_COLOR)) {
770 supportedChannelIdSet.add(CHANNEL_2_COLOR_XY_ONLY);
772 supportedChannelIdSet.remove(CHANNEL_2_BRIGHTNESS);
773 supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
775 supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
776 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
778 if (supportedChannelIdSet.contains(CHANNEL_2_BRIGHTNESS)) {
779 supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
781 supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
782 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
784 if (supportedChannelIdSet.contains(CHANNEL_2_SWITCH)) {
785 supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
789 * This binding creates its dynamic list of channels by a 'subtractive' method i.e. the full set of
790 * channels is initially created from the thing type xml, and then for any channels where UndfType.NULL
791 * data is returned, the respective channel is removed from the full list. However in seldom cases
792 * UndfType.NULL may wrongly be returned, so we should log a warning here just in case.
794 if (logger.isDebugEnabled()) {
795 supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
796 .forEach(channelId -> logger.debug(
797 "{} -> updateChannelList() required channel '{}' missing", resourceId, channelId));
799 supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
800 .forEach(channelId -> logger.warn(
801 "Thing '{}' is missing required channel '{}'. Please recreate the thing!",
802 thing.getUID(), channelId));
805 // get list of unused channels
806 List<Channel> unusedChannels = thing.getChannels().stream()
807 .filter(channel -> !supportedChannelIdSet.contains(channel.getUID().getId()))
808 .collect(Collectors.toList());
810 // remove any unused channels
811 if (!unusedChannels.isEmpty()) {
812 if (logger.isDebugEnabled()) {
813 unusedChannels.stream().map(channel -> channel.getUID().getId())
814 .forEach(channelId -> logger.debug(
815 "{} -> updateChannelList() removing unused channel '{}'", resourceId,
818 updateThing(editThing().withoutChannels(unusedChannels).build());
825 * Update the state of the existing channels.
827 * @param resource the Resource containing the new channel state.
828 * @return true if the channel was found and updated.
830 private boolean updateChannels(Resource resource) {
831 logger.debug("{} -> updateChannels() from resource {}", resourceId, resource);
832 boolean fullUpdate = resource.hasFullState();
833 switch (resource.getType()) {
836 addSupportedChannel(CHANNEL_2_BUTTON_LAST_EVENT);
837 addSupportedChannel(CHANNEL_2_BUTTON_LAST_UPDATED);
838 controlIds.put(resource.getId(), resource.getControlId());
840 State buttonState = resource.getButtonEventState(controlIds);
841 updateState(CHANNEL_2_BUTTON_LAST_EVENT, buttonState, fullUpdate);
843 // Update channel from timestamp if last button pressed.
844 State buttonLastUpdatedState = resource.getButtonLastUpdatedState(timeZoneProvider.getTimeZone());
845 if (buttonLastUpdatedState instanceof DateTimeType) {
846 Instant buttonLastUpdatedInstant = ((DateTimeType) buttonLastUpdatedState).getInstant();
847 if (buttonLastUpdatedInstant.isAfter(buttonGroupLastUpdated)) {
848 updateState(CHANNEL_2_BUTTON_LAST_UPDATED, buttonLastUpdatedState, fullUpdate);
849 buttonGroupLastUpdated = buttonLastUpdatedInstant;
851 } else if (Instant.MIN.equals(buttonGroupLastUpdated)) {
852 updateState(CHANNEL_2_BUTTON_LAST_UPDATED, buttonLastUpdatedState, fullUpdate);
857 updateState(CHANNEL_2_BATTERY_LEVEL, resource.getBatteryLevelState(), fullUpdate);
858 updateState(CHANNEL_2_BATTERY_LOW, resource.getBatteryLowState(), fullUpdate);
863 updateEffectChannel(resource);
865 updateState(CHANNEL_2_COLOR_TEMP_PERCENT, resource.getColorTemperaturePercentState(), fullUpdate);
866 updateState(CHANNEL_2_COLOR_TEMP_ABSOLUTE, resource.getColorTemperatureAbsoluteState(), fullUpdate);
867 updateState(CHANNEL_2_COLOR, resource.getColorState(), fullUpdate);
868 updateState(CHANNEL_2_COLOR_XY_ONLY, resource.getColorXyState(), fullUpdate);
869 updateState(CHANNEL_2_EFFECT, resource.getEffectState(), fullUpdate);
870 // fall through for dimming and on/off related channels
874 updateAlertChannel(resource);
876 updateState(CHANNEL_2_BRIGHTNESS, resource.getBrightnessState(), fullUpdate);
877 updateState(CHANNEL_2_DIMMING_ONLY, resource.getDimmingState(), fullUpdate);
878 updateState(CHANNEL_2_SWITCH, resource.getOnOffState(), fullUpdate);
879 updateState(CHANNEL_2_ON_OFF_ONLY, resource.getOnOffState(), fullUpdate);
880 updateState(CHANNEL_2_ALERT, resource.getAlertState(), fullUpdate);
884 updateState(CHANNEL_2_LIGHT_LEVEL, resource.getLightLevelState(), fullUpdate);
885 updateState(CHANNEL_2_LIGHT_LEVEL_LAST_UPDATED,
886 resource.getLightLevelLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
887 updateState(CHANNEL_2_LIGHT_LEVEL_ENABLED, resource.getEnabledState(), fullUpdate);
891 updateState(CHANNEL_2_MOTION, resource.getMotionState(), fullUpdate);
892 updateState(CHANNEL_2_MOTION_LAST_UPDATED,
893 resource.getMotionLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
894 updateState(CHANNEL_2_MOTION_ENABLED, resource.getEnabledState(), fullUpdate);
897 case RELATIVE_ROTARY:
899 addSupportedChannel(CHANNEL_2_ROTARY_STEPS);
900 addSupportedChannel(CHANNEL_2_ROTARY_STEPS_LAST_UPDATED);
902 updateState(CHANNEL_2_ROTARY_STEPS, resource.getRotaryStepsState(), fullUpdate);
904 updateState(CHANNEL_2_ROTARY_STEPS_LAST_UPDATED,
905 resource.getRotaryStepsLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
909 updateState(CHANNEL_2_TEMPERATURE, resource.getTemperatureState(), fullUpdate);
910 updateState(CHANNEL_2_TEMPERATURE_LAST_UPDATED,
911 resource.getTemperatureLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate);
912 updateState(CHANNEL_2_TEMPERATURE_ENABLED, resource.getEnabledState(), fullUpdate);
915 case ZIGBEE_CONNECTIVITY:
916 updateConnectivityState(resource);
920 updateState(CHANNEL_2_SCENE, resource.getSceneState(), fullUpdate);
924 updateState(CHANNEL_2_SCENE, resource.getSmartSceneState(), fullUpdate);
930 if (thisResource.getType() == ResourceType.DEVICE) {
931 updateState(CHANNEL_2_LAST_UPDATED, new DateTimeType(), fullUpdate);
937 * Check the Zigbee connectivity and set the thing online status accordingly. If the thing is offline then set all
938 * its channel states to undefined, otherwise execute a refresh command to update channels to the latest current
941 * @param resource a Resource that potentially contains the Zigbee connectivity state.
943 private void updateConnectivityState(Resource resource) {
944 ZigbeeStatus zigbeeStatus = resource.getZigbeeStatus();
945 if (Objects.nonNull(zigbeeStatus)) {
946 logger.debug("{} -> updateConnectivityState() thingStatus:{}, zigbeeStatus:{}", resourceId,
947 thing.getStatus(), zigbeeStatus);
948 hasConnectivityIssue = zigbeeStatus != ZigbeeStatus.CONNECTED;
949 if (hasConnectivityIssue) {
950 if (thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.COMMUNICATION_ERROR) {
951 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
952 "@text/offline.api2.comm-error.zigbee-connectivity-issue");
953 supportedChannelIdSet.forEach(channelId -> updateState(channelId, UnDefType.UNDEF));
955 } else if (thing.getStatus() != ThingStatus.ONLINE) {
956 updateStatus(ThingStatus.ONLINE);
957 refreshAllChannels();
963 * Get all resources needed for building the thing state. Build the forward / reverse contributor lookup maps. Set
964 * up the final list of channels in the thing.
966 private synchronized void updateDependencies() {
967 if (!disposing && !updateDependenciesDone) {
968 logger.debug("{} -> updateDependencies()", resourceId);
970 if (!updatePropertiesDone) {
971 logger.debug("{} -> updateDependencies() properties not initialized", resourceId);
974 if (!updateSceneContributorsDone && !updateSceneContributors()) {
975 logger.debug("{} -> updateDependencies() scenes not initialized", resourceId);
979 updateServiceContributors();
981 updateChannelItemLinksFromLegacy();
982 if (!hasConnectivityIssue) {
983 updateStatus(ThingStatus.ONLINE);
985 updateDependenciesDone = true;
986 } catch (ApiException e) {
987 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
988 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
989 } catch (AssetNotLoadedException e) {
990 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
991 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
992 "@text/offline.api2.conf-error.assets-not-loaded");
993 } catch (InterruptedException e) {
999 * Process the incoming Resource to initialize the fixed resp. timed effects channel.
1001 * @param resource a Resource possibly containing a fixed and/or timed effects element.
1003 public void updateEffectChannel(Resource resource) {
1004 Effects fixedEffects = resource.getFixedEffects();
1005 TimedEffects timedEffects = resource.getTimedEffects();
1006 List<StateOption> stateOptions = Stream
1007 .concat(Objects.nonNull(fixedEffects) ? fixedEffects.getStatusValues().stream() : Stream.empty(),
1008 Objects.nonNull(timedEffects) ? timedEffects.getStatusValues().stream() : Stream.empty())
1010 String effectName = EffectType.of(effect).name();
1011 return new StateOption(effectName, effectName);
1012 }).distinct().collect(Collectors.toList());
1013 if (!stateOptions.isEmpty()) {
1014 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT), stateOptions);
1015 logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size());
1020 * Update the light properties.
1022 * @param resource a Resource object containing the property data.
1024 private synchronized void updateLightProperties(Resource resource) {
1025 if (!disposing && !updateLightPropertiesDone) {
1026 logger.debug("{} -> updateLightProperties()", resourceId);
1028 Dimming dimming = resource.getDimming();
1029 thing.setProperty(PROPERTY_DIMMING_RANGE, Objects.nonNull(dimming) ? dimming.toPropertyValue() : null);
1031 MirekSchema mirekSchema = resource.getMirekSchema();
1032 thing.setProperty(PROPERTY_COLOR_TEMP_RANGE,
1033 Objects.nonNull(mirekSchema) ? mirekSchema.toPropertyValue() : null);
1035 ColorXy colorXy = resource.getColorXy();
1036 Gamut2 gamut = Objects.nonNull(colorXy) ? colorXy.getGamut2() : null;
1037 thing.setProperty(PROPERTY_COLOR_GAMUT, Objects.nonNull(gamut) ? gamut.toPropertyValue() : null);
1039 updateLightPropertiesDone = true;
1044 * Initialize the lookup maps of resources that contribute to the thing state.
1046 private void updateLookups() {
1048 logger.debug("{} -> updateLookups()", resourceId);
1049 // get supported services
1050 List<ResourceReference> services = thisResource.getServiceReferences();
1052 // add supported services to contributorsCache
1053 serviceContributorsCache.clear();
1054 serviceContributorsCache.putAll(services.stream()
1055 .collect(Collectors.toMap(ResourceReference::getId, r -> new Resource(r.getType()))));
1057 // add supported services to commandResourceIds
1058 commandResourceIds.clear();
1059 commandResourceIds.putAll(services.stream() // use a 'mergeFunction' to prevent duplicates
1060 .collect(Collectors.toMap(ResourceReference::getType, ResourceReference::getId, (r1, r2) -> r1)));
1065 * Update the primary device properties.
1067 * @param resource a Resource object containing the property data.
1069 private synchronized void updateProperties(Resource resource) {
1070 if (!disposing && !updatePropertiesDone) {
1071 logger.debug("{} -> updateProperties()", resourceId);
1072 Map<String, String> properties = new HashMap<>(thing.getProperties());
1075 properties.put(PROPERTY_RESOURCE_TYPE, thisResource.getType().toString());
1076 properties.put(PROPERTY_RESOURCE_NAME, thisResource.getName());
1078 // owner information
1079 ResourceReference owner = thisResource.getOwner();
1080 if (Objects.nonNull(owner)) {
1081 String ownerId = owner.getId();
1082 if (Objects.nonNull(ownerId)) {
1083 properties.put(PROPERTY_OWNER, ownerId);
1085 ResourceType ownerType = owner.getType();
1086 properties.put(PROPERTY_OWNER_TYPE, ownerType.toString());
1090 MetaData metaData = thisResource.getMetaData();
1091 if (Objects.nonNull(metaData)) {
1092 properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString());
1096 ProductData productData = thisResource.getProductData();
1097 if (Objects.nonNull(productData)) {
1098 String modelId = productData.getModelId();
1100 // standard properties
1101 properties.put(PROPERTY_RESOURCE_ID, resourceId);
1102 properties.put(Thing.PROPERTY_MODEL_ID, modelId);
1103 properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName());
1104 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion());
1105 String hardwarePlatformType = productData.getHardwarePlatformType();
1106 if (Objects.nonNull(hardwarePlatformType)) {
1107 properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType);
1110 // hue specific properties
1111 properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName());
1112 properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString());
1113 properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString());
1115 // Check device for needed work-arounds.
1116 if (LK_WISER_DIMMER_MODEL_ID.equals(modelId)) {
1117 // Apply transition time as a workaround for LK Wiser Dimmer firmware bug.
1118 // Additional details here: https://techblog.vindvejr.dk/?p=455
1119 applyOffTransitionWorkaround = true;
1120 logger.debug("{} -> enabling work-around for turning off LK Wiser Dimmer", resourceId);
1124 thing.setProperties(properties);
1125 updatePropertiesDone = true;
1130 * Execute an HTTP GET command to fetch the resources data for the referenced resource.
1132 * @param reference to the required resource.
1133 * @throws ApiException if a communication error occurred.
1134 * @throws AssetNotLoadedException if one of the assets is not loaded.
1135 * @throws InterruptedException
1137 private void updateResource(ResourceReference reference)
1138 throws ApiException, AssetNotLoadedException, InterruptedException {
1140 logger.debug("{} -> updateResource() from resource {}", resourceId, reference);
1141 getBridgeHandler().getResources(reference).getResources().stream()
1142 .forEach(resource -> onResource(resource));
1147 * Fetch the full list of normal resp. smart scenes from the bridge, and call
1148 * {@code updateSceneContributors(List<Resource> allScenes)}
1150 * @throws ApiException if a communication error occurred.
1151 * @throws AssetNotLoadedException if one of the assets is not loaded.
1152 * @throws InterruptedException
1154 public boolean updateSceneContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1155 if (!disposing && !updateSceneContributorsDone) {
1156 List<Resource> allScenes = new ArrayList<>();
1157 for (ResourceType type : SUPPORTED_SCENE_TYPES) {
1158 allScenes.addAll(getBridgeHandler().getResources(new ResourceReference().setType(type)).getResources());
1160 updateSceneContributors(allScenes);
1162 return updateSceneContributorsDone;
1166 * Process the incoming list of normal resp. smart scene resources to find those which contribute to this thing. And
1167 * if there are any, include a scene channel in the supported channel list, and populate its respective state
1170 * @param allScenes the full list of normal resp. smart scene resources.
1172 public synchronized boolean updateSceneContributors(List<Resource> allScenes) {
1173 if (!disposing && !updateSceneContributorsDone) {
1174 sceneContributorsCache.clear();
1175 sceneResourceEntries.clear();
1177 ResourceReference thisReference = getResourceReference();
1178 List<Resource> scenes = allScenes.stream().filter(s -> thisReference.equals(s.getGroup()))
1179 .collect(Collectors.toList());
1181 if (!scenes.isEmpty()) {
1182 sceneContributorsCache.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)));
1183 sceneResourceEntries.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s)));
1185 State state = scenes.stream().filter(s -> s.getSceneActive().orElse(false)).map(s -> s.getSceneState())
1186 .findAny().orElse(UnDefType.UNDEF);
1188 updateState(CHANNEL_2_SCENE, state, true);
1190 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_SCENE), scenes
1191 .stream().map(s -> s.getName()).map(n -> new StateOption(n, n)).collect(Collectors.toList()));
1193 logger.debug("{} -> updateSceneContributors() found {} normal resp. smart scenes", resourceId,
1196 updateSceneContributorsDone = true;
1198 return updateSceneContributorsDone;
1202 * Execute a series of HTTP GET commands to fetch the resource data for all service resources that contribute to the
1205 * @throws ApiException if a communication error occurred.
1206 * @throws AssetNotLoadedException if one of the assets is not loaded.
1207 * @throws InterruptedException
1209 private void updateServiceContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1211 logger.debug("{} -> updateServiceContributors() called for {} contributors", resourceId,
1212 serviceContributorsCache.size());
1213 ResourceReference reference = new ResourceReference();
1214 for (var entry : serviceContributorsCache.entrySet()) {
1215 updateResource(reference.setId(entry.getKey()).setType(entry.getValue().getType()));
1221 * Update the channel state, and if appropriate add the channel ID to the set of supportedChannelIds. Calls either
1222 * OH core updateState() or triggerChannel() methods depending on the channel kind.
1224 * Note: the particular 'UnDefType.UNDEF' value of the state argument is used to specially indicate the undefined
1225 * state, but yet that its channel shall nevertheless continue to be present in the thing.
1227 * @param channelID the id of the channel.
1228 * @param state the new state of the channel.
1229 * @param fullUpdate if true always update the channel, otherwise only update if state is not 'UNDEF'.
1231 private void updateState(String channelID, State state, boolean fullUpdate) {
1232 boolean isDefined = state != UnDefType.NULL;
1233 Channel channel = thing.getChannel(channelID);
1235 if ((fullUpdate || isDefined) && Objects.nonNull(channel)) {
1236 logger.debug("{} -> updateState() '{}' update with '{}' (fullUpdate:{}, isDefined:{})", resourceId,
1237 channelID, state, fullUpdate, isDefined);
1239 switch (channel.getKind()) {
1241 updateState(channelID, state);
1245 if (state instanceof DecimalType) {
1246 triggerChannel(channelID, String.valueOf(((DecimalType) state).intValue()));
1250 if (fullUpdate && isDefined) {
1251 addSupportedChannel(channelID);
1256 * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's
1257 * settings into this thing.
1259 private void updateThingFromLegacy() {
1260 if (isInitialized()) {
1261 logger.warn("Cannot update thing '{}' from legacy thing since handler already initialized.",
1265 Map<String, String> properties = thing.getProperties();
1266 String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID);
1267 if (Objects.nonNull(legacyThingUID)) {
1268 Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID));
1269 if (Objects.nonNull(legacyThing)) {
1270 ThingBuilder editBuilder = editThing();
1272 String location = legacyThing.getLocation();
1273 if (Objects.nonNull(location) && !location.isBlank()) {
1274 editBuilder = editBuilder.withLocation(location);
1277 // save list of legacyLinkedChannelUIDs for use after channel list is initialised
1278 legacyLinkedChannelUIDs.clear();
1279 legacyLinkedChannelUIDs.addAll(legacyThing.getChannels().stream().map(Channel::getUID)
1280 .filter(uid -> REPLICATE_CHANNEL_ID_MAP.containsKey(uid.getId())
1281 && itemChannelLinkRegistry.isLinked(uid))
1282 .collect(Collectors.toList()));
1284 Map<String, String> newProperties = new HashMap<>(properties);
1285 newProperties.remove(PROPERTY_LEGACY_THING_UID);
1287 updateThing(editBuilder.withProperties(newProperties).build());