]> git.basschouten.com Git - openhab-addons.git/blob
00975e55d2c1afbecdc637eccea05dbb09d24aa4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hue.internal.handler;
14
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Set;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.TimeUnit;
31 import java.util.stream.Collectors;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.hue.internal.action.DynamicsActions;
36 import org.openhab.binding.hue.internal.config.Clip2ThingConfig;
37 import org.openhab.binding.hue.internal.dto.clip2.Alerts;
38 import org.openhab.binding.hue.internal.dto.clip2.ColorXy;
39 import org.openhab.binding.hue.internal.dto.clip2.Dimming;
40 import org.openhab.binding.hue.internal.dto.clip2.Effects;
41 import org.openhab.binding.hue.internal.dto.clip2.Gamut2;
42 import org.openhab.binding.hue.internal.dto.clip2.MetaData;
43 import org.openhab.binding.hue.internal.dto.clip2.MirekSchema;
44 import org.openhab.binding.hue.internal.dto.clip2.ProductData;
45 import org.openhab.binding.hue.internal.dto.clip2.Resource;
46 import org.openhab.binding.hue.internal.dto.clip2.ResourceReference;
47 import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
48 import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
49 import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
50 import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
51 import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
52 import org.openhab.binding.hue.internal.dto.clip2.helper.Setters;
53 import org.openhab.binding.hue.internal.exceptions.ApiException;
54 import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
55 import org.openhab.core.library.types.DateTimeType;
56 import org.openhab.core.library.types.DecimalType;
57 import org.openhab.core.library.types.HSBType;
58 import org.openhab.core.library.types.IncreaseDecreaseType;
59 import org.openhab.core.library.types.OnOffType;
60 import org.openhab.core.library.types.PercentType;
61 import org.openhab.core.library.types.QuantityType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.library.unit.MetricPrefix;
64 import org.openhab.core.library.unit.Units;
65 import org.openhab.core.thing.Bridge;
66 import org.openhab.core.thing.Channel;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingRegistry;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingTypeUID;
73 import org.openhab.core.thing.ThingUID;
74 import org.openhab.core.thing.binding.BaseThingHandler;
75 import org.openhab.core.thing.binding.BridgeHandler;
76 import org.openhab.core.thing.binding.ThingHandlerService;
77 import org.openhab.core.thing.binding.builder.ThingBuilder;
78 import org.openhab.core.thing.link.ItemChannelLink;
79 import org.openhab.core.thing.link.ItemChannelLinkRegistry;
80 import org.openhab.core.types.Command;
81 import org.openhab.core.types.RefreshType;
82 import org.openhab.core.types.State;
83 import org.openhab.core.types.StateOption;
84 import org.openhab.core.types.UnDefType;
85 import org.slf4j.Logger;
86 import org.slf4j.LoggerFactory;
87
88 /**
89  * Handler for things based on CLIP 2 'device', 'room', or 'zone resources.
90  *
91  * @author Andrew Fiddian-Green - Initial contribution.
92  */
93 @NonNullByDefault
94 public class Clip2ThingHandler extends BaseThingHandler {
95
96     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM,
97             THING_TYPE_ZONE);
98
99     private static final Duration DYNAMICS_ACTIVE_WINDOW = Duration.ofSeconds(10);
100
101     private final Logger logger = LoggerFactory.getLogger(Clip2ThingHandler.class);
102
103     /**
104      * A map of service Resources whose state contributes to the overall state of this thing. It is a map between the
105      * resource ID (string) and a Resource object containing the last known state. e.g. a DEVICE thing may support a
106      * LIGHT service whose Resource contributes to its overall state, or a ROOM or ZONE thing may support a
107      * GROUPED_LIGHT service whose Resource contributes to the its overall state.
108      */
109     private final Map<String, Resource> serviceContributorsCache = new ConcurrentHashMap<>();
110
111     /**
112      * A map of Resource IDs which are targets for commands to be sent. It is a map between the type of command
113      * (ResourcesType) and the resource ID to which the command shall be sent. e.g. a LIGHT 'on' command shall be sent
114      * to the respective LIGHT resource ID.
115      */
116     private final Map<ResourceType, String> commandResourceIds = new ConcurrentHashMap<>();
117
118     /**
119      * Button devices contain one or more physical buttons, each of which is represented by a BUTTON Resource with its
120      * own unique resource ID, and a respective controlId that indicates which button it is in the device. e.g. a dimmer
121      * pad has four buttons (controlId's 1..4) each represented by a BUTTON Resource with a unique resource ID. This is
122      * a map between the resource ID and its respective controlId.
123      */
124     private final Map<String, Integer> controlIds = new ConcurrentHashMap<>();
125
126     /**
127      * The set of channel IDs that are supported by this thing. e.g. an on/off light may support 'switch' and
128      * 'zigbeeStatus' channels, whereas a complex light may support 'switch', 'brightness', 'color', 'color temperature'
129      * and 'zigbeeStatus' channels.
130      */
131     private final Set<String> supportedChannelIdSet = new HashSet<>();
132
133     /**
134      * A map of scene IDs and respective scene Resources for the scenes that contribute to and command this thing. It is
135      * a map between the resource ID (string) and a Resource object containing the scene's last known state.
136      */
137     private final Map<String, Resource> sceneContributorsCache = new ConcurrentHashMap<>();
138
139     /**
140      * A map of scene names versus Resource IDs for the scenes that contribute to and command this thing. e.g. a command
141      * for a scene named 'Energize' shall be sent to the respective SCENE resource ID.
142      */
143     private final Map<String, String> sceneResourceIds = new ConcurrentHashMap<>();
144
145     /**
146      * A list of API v1 thing channel UIDs that are linked to items. It is used in the process of replicating the
147      * Item/Channel links from a legacy v1 thing to this API v2 thing.
148      */
149     private final List<ChannelUID> legacyLinkedChannelUIDs = new CopyOnWriteArrayList<>();
150
151     private final ThingRegistry thingRegistry;
152     private final ItemChannelLinkRegistry itemChannelLinkRegistry;
153     private final Clip2StateDescriptionProvider stateDescriptionProvider;
154
155     private String resourceId = "?";
156     private Resource thisResource;
157     private Duration dynamicsDuration = Duration.ZERO;
158     private Instant dynamicsExpireTime = Instant.MIN;
159
160     private boolean disposing;
161     private boolean hasConnectivityIssue;
162     private boolean updateSceneContributorsDone;
163     private boolean updateLightPropertiesDone;
164     private boolean updatePropertiesDone;
165     private boolean updateDependenciesDone;
166
167     private @Nullable Future<?> alertResetTask;
168     private @Nullable Future<?> dynamicsResetTask;
169     private @Nullable Future<?> updateDependenciesTask;
170     private @Nullable Future<?> updateServiceContributorsTask;
171
172     public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescriptionProvider,
173             ThingRegistry thingRegistry, ItemChannelLinkRegistry itemChannelLinkRegistry) {
174         super(thing);
175
176         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
177         if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
178             thisResource = new Resource(ResourceType.DEVICE);
179         } else if (THING_TYPE_ROOM.equals(thingTypeUID)) {
180             thisResource = new Resource(ResourceType.ROOM);
181         } else if (THING_TYPE_ZONE.equals(thingTypeUID)) {
182             thisResource = new Resource(ResourceType.ZONE);
183         } else {
184             throw new IllegalArgumentException("Wrong thing type " + thingTypeUID.getAsString());
185         }
186
187         this.thingRegistry = thingRegistry;
188         this.itemChannelLinkRegistry = itemChannelLinkRegistry;
189         this.stateDescriptionProvider = stateDescriptionProvider;
190     }
191
192     /**
193      * Add a channel ID to the supportedChannelIdSet set. If the channel supports dynamics (timed transitions) then add
194      * the respective channel as well.
195      *
196      * @param channelId the channel ID to add.
197      */
198     private void addSupportedChannel(String channelId) {
199         if (!disposing && !updateDependenciesDone) {
200             synchronized (supportedChannelIdSet) {
201                 logger.debug("{} -> addSupportedChannel() '{}' added to supported channel set", resourceId, channelId);
202                 supportedChannelIdSet.add(channelId);
203                 if (DYNAMIC_CHANNELS.contains(channelId)) {
204                     clearDynamicsChannel();
205                 }
206             }
207         }
208     }
209
210     /**
211      * Cancel the given task.
212      *
213      * @param cancelTask the task to be cancelled (may be null)
214      * @param mayInterrupt allows cancel() to interrupt the thread.
215      */
216     private void cancelTask(@Nullable Future<?> cancelTask, boolean mayInterrupt) {
217         if (Objects.nonNull(cancelTask)) {
218             cancelTask.cancel(mayInterrupt);
219         }
220     }
221
222     /**
223      * Clear the dynamics channel parameters.
224      */
225     private void clearDynamicsChannel() {
226         dynamicsExpireTime = Instant.MIN;
227         dynamicsDuration = Duration.ZERO;
228         updateState(CHANNEL_2_DYNAMICS, new QuantityType<>(0, MetricPrefix.MILLI(Units.SECOND)), true);
229     }
230
231     @Override
232     public void dispose() {
233         logger.debug("{} -> dispose()", resourceId);
234         disposing = true;
235         cancelTask(alertResetTask, true);
236         cancelTask(dynamicsResetTask, true);
237         cancelTask(updateDependenciesTask, true);
238         cancelTask(updateServiceContributorsTask, true);
239         alertResetTask = null;
240         dynamicsResetTask = null;
241         updateDependenciesTask = null;
242         updateServiceContributorsTask = null;
243         legacyLinkedChannelUIDs.clear();
244         sceneContributorsCache.clear();
245         sceneResourceIds.clear();
246         supportedChannelIdSet.clear();
247         commandResourceIds.clear();
248         serviceContributorsCache.clear();
249         controlIds.clear();
250     }
251
252     /**
253      * Get the bridge handler.
254      *
255      * @throws AssetNotLoadedException if the handler does not exist.
256      */
257     private Clip2BridgeHandler getBridgeHandler() throws AssetNotLoadedException {
258         Bridge bridge = getBridge();
259         if (Objects.nonNull(bridge)) {
260             BridgeHandler handler = bridge.getHandler();
261             if (handler instanceof Clip2BridgeHandler) {
262                 return (Clip2BridgeHandler) handler;
263             }
264         }
265         throw new AssetNotLoadedException("Bridge handler missing");
266     }
267
268     /**
269      * Do a double lookup to get the cached resource that matches the given ResourceType.
270      *
271      * @param resourceType the type to search for.
272      * @return the Resource, or null if not found.
273      */
274     private @Nullable Resource getCachedResource(ResourceType resourceType) {
275         String commandResourceId = commandResourceIds.get(resourceType);
276         return Objects.nonNull(commandResourceId) ? serviceContributorsCache.get(commandResourceId) : null;
277     }
278
279     /**
280      * Return a ResourceReference to this handler's resource.
281      *
282      * @return a ResourceReference instance.
283      */
284     public ResourceReference getResourceReference() {
285         return new ResourceReference().setId(resourceId).setType(thisResource.getType());
286     }
287
288     /**
289      * Register the 'DynamicsAction' service.
290      */
291     @Override
292     public Collection<Class<? extends ThingHandlerService>> getServices() {
293         return Set.of(DynamicsActions.class);
294     }
295
296     @Override
297     public void handleCommand(ChannelUID channelUID, Command commandParam) {
298         if (RefreshType.REFRESH.equals(commandParam)) {
299             if ((thing.getStatus() == ThingStatus.ONLINE) && updateDependenciesDone) {
300                 Future<?> task = updateServiceContributorsTask;
301                 if (Objects.isNull(task) || !task.isDone()) {
302                     cancelTask(updateServiceContributorsTask, false);
303                     updateServiceContributorsTask = scheduler.schedule(() -> {
304                         try {
305                             updateServiceContributors();
306                         } catch (ApiException | AssetNotLoadedException e) {
307                             logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
308                         } catch (InterruptedException e) {
309                         }
310                     }, 3, TimeUnit.SECONDS);
311                 }
312             }
313             return;
314         }
315
316         Channel channel = thing.getChannel(channelUID);
317         if (channel == null) {
318             if (logger.isDebugEnabled()) {
319                 logger.debug("{} -> handleCommand() channelUID:{} does not exist", resourceId, channelUID);
320
321             } else {
322                 logger.warn("Command received for channel '{}' which is not in thing '{}'.", channelUID,
323                         thing.getUID());
324             }
325             return;
326         }
327
328         ResourceType lightResourceType = thisResource.getType() == ResourceType.DEVICE ? ResourceType.LIGHT
329                 : ResourceType.GROUPED_LIGHT;
330
331         Resource putResource = null;
332         String putResourceId = null;
333         Command command = commandParam;
334         String channelId = channelUID.getId();
335         Resource cache = getCachedResource(lightResourceType);
336
337         switch (channelId) {
338             case CHANNEL_2_ALERT:
339                 putResource = Setters.setAlert(new Resource(lightResourceType), command, cache);
340                 cancelTask(alertResetTask, false);
341                 alertResetTask = scheduler.schedule(
342                         () -> updateState(channelUID, new StringType(ActionType.NO_ACTION.name())), 10,
343                         TimeUnit.SECONDS);
344                 break;
345
346             case CHANNEL_2_EFFECT:
347                 putResource = Setters.setEffect(new Resource(lightResourceType), command, cache);
348                 putResource.setOnOff(OnOffType.ON);
349                 break;
350
351             case CHANNEL_2_COLOR_TEMP_PERCENT:
352                 if (command instanceof IncreaseDecreaseType) {
353                     if (Objects.nonNull(cache)) {
354                         State current = cache.getColorTemperaturePercentState();
355                         if (current instanceof PercentType) {
356                             int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1;
357                             int percent = ((PercentType) current).intValue() + (sign * (int) Resource.PERCENT_DELTA);
358                             command = new PercentType(Math.min(100, Math.max(0, percent)));
359                         }
360                     }
361                 } else if (command instanceof OnOffType) {
362                     command = OnOffType.OFF == command ? PercentType.ZERO : PercentType.HUNDRED;
363                 }
364                 putResource = Setters.setColorTemperaturePercent(new Resource(lightResourceType), command, cache);
365                 break;
366
367             case CHANNEL_2_COLOR_TEMP_ABSOLUTE:
368                 putResource = Setters.setColorTemperatureAbsolute(new Resource(lightResourceType), command, cache);
369                 break;
370
371             case CHANNEL_2_COLOR:
372                 putResource = new Resource(lightResourceType);
373                 if (command instanceof HSBType) {
374                     HSBType color = ((HSBType) command);
375                     putResource = Setters.setColorXy(putResource, color, cache);
376                     command = color.getBrightness();
377                 }
378                 // NB fall through for handling of brightness and switch related commands !!
379
380             case CHANNEL_2_BRIGHTNESS:
381                 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
382                 if (command instanceof IncreaseDecreaseType) {
383                     if (Objects.nonNull(cache)) {
384                         State current = cache.getBrightnessState();
385                         if (current instanceof PercentType) {
386                             int sign = IncreaseDecreaseType.INCREASE == command ? 1 : -1;
387                             double percent = ((PercentType) current).doubleValue() + (sign * Resource.PERCENT_DELTA);
388                             command = new PercentType(new BigDecimal(Math.min(100f, Math.max(0f, percent)),
389                                     Resource.PERCENT_MATH_CONTEXT));
390                         }
391                     }
392                 }
393                 if (command instanceof PercentType) {
394                     PercentType brightness = (PercentType) command;
395                     putResource = Setters.setDimming(putResource, brightness, cache);
396                     Double minDimLevel = Objects.nonNull(cache) ? cache.getMinimumDimmingLevel() : null;
397                     minDimLevel = Objects.nonNull(minDimLevel) ? minDimLevel : Dimming.DEFAULT_MINIMUM_DIMMIMG_LEVEL;
398                     command = OnOffType.from(brightness.doubleValue() >= minDimLevel);
399                 }
400                 // NB fall through for handling of switch related commands !!
401
402             case CHANNEL_2_SWITCH:
403                 putResource = Objects.nonNull(putResource) ? putResource : new Resource(lightResourceType);
404                 putResource.setOnOff(command);
405                 break;
406
407             case CHANNEL_2_COLOR_XY_ONLY:
408                 putResource = Setters.setColorXy(new Resource(lightResourceType), command, cache);
409                 break;
410
411             case CHANNEL_2_DIMMING_ONLY:
412                 putResource = Setters.setDimming(new Resource(lightResourceType), command, cache);
413                 break;
414
415             case CHANNEL_2_ON_OFF_ONLY:
416                 putResource = new Resource(lightResourceType).setOnOff(command);
417                 break;
418
419             case CHANNEL_2_TEMPERATURE_ENABLED:
420                 putResource = new Resource(ResourceType.TEMPERATURE).setEnabled(command);
421                 break;
422
423             case CHANNEL_2_MOTION_ENABLED:
424                 putResource = new Resource(ResourceType.MOTION).setEnabled(command);
425                 break;
426
427             case CHANNEL_2_LIGHT_LEVEL_ENABLED:
428                 putResource = new Resource(ResourceType.LIGHT_LEVEL).setEnabled(command);
429                 break;
430
431             case CHANNEL_2_SCENE:
432                 if (command instanceof StringType) {
433                     putResourceId = sceneResourceIds.get(((StringType) command).toString());
434                     if (Objects.nonNull(putResourceId)) {
435                         putResource = new Resource(ResourceType.SCENE).setRecallAction(RecallAction.ACTIVE);
436                     }
437                 }
438                 break;
439
440             case CHANNEL_2_DYNAMICS:
441                 Duration clearAfter = Duration.ZERO;
442                 if (command instanceof QuantityType<?>) {
443                     QuantityType<?> durationMs = ((QuantityType<?>) command).toUnit(MetricPrefix.MILLI(Units.SECOND));
444                     if (Objects.nonNull(durationMs) && durationMs.longValue() > 0) {
445                         Duration duration = Duration.ofMillis(durationMs.longValue());
446                         dynamicsDuration = duration;
447                         dynamicsExpireTime = Instant.now().plus(DYNAMICS_ACTIVE_WINDOW);
448                         clearAfter = DYNAMICS_ACTIVE_WINDOW;
449                         logger.debug("{} -> handleCommand() dynamics setting {} valid for {}", resourceId, duration,
450                                 clearAfter);
451                     }
452                 }
453                 cancelTask(dynamicsResetTask, false);
454                 dynamicsResetTask = scheduler.schedule(() -> clearDynamicsChannel(), clearAfter.toMillis(),
455                         TimeUnit.MILLISECONDS);
456                 return;
457
458             default:
459                 if (logger.isDebugEnabled()) {
460                     logger.debug("{} -> handleCommand() channelUID:{} unknown", resourceId, channelUID);
461                 } else {
462                     logger.warn("Command received for unknown channel '{}'.", channelUID);
463                 }
464                 return;
465         }
466
467         if (putResource == null) {
468             if (logger.isDebugEnabled()) {
469                 logger.debug("{} -> handleCommand() command:{} not supported on channelUID:{}", resourceId, command,
470                         channelUID);
471             } else {
472                 logger.warn("Command '{}' is not supported on channel '{}'.", command, channelUID);
473             }
474             return;
475         }
476
477         putResourceId = Objects.nonNull(putResourceId) ? putResourceId : commandResourceIds.get(putResource.getType());
478         if (putResourceId == null) {
479             if (logger.isDebugEnabled()) {
480                 logger.debug(
481                         "{} -> handleCommand() channelUID:{}, command:{}, putResourceType:{} => missing resource ID",
482                         resourceId, channelUID, command, putResource.getType());
483             } else {
484                 logger.warn("Command '{}' for channel '{}' cannot be processed by thing '{}'.", command, channelUID,
485                         thing.getUID());
486             }
487             return;
488         }
489
490         if (DYNAMIC_CHANNELS.contains(channelId)) {
491             if (Instant.now().isBefore(dynamicsExpireTime) && !dynamicsDuration.isZero()
492                     && !dynamicsDuration.isNegative()) {
493                 if (ResourceType.SCENE == putResource.getType()) {
494                     putResource.setRecallDuration(dynamicsDuration);
495                 } else {
496                     putResource.setDynamicsDuration(dynamicsDuration);
497                 }
498             }
499         }
500
501         putResource.setId(putResourceId);
502         logger.debug("{} -> handleCommand() put resource {}", resourceId, putResource);
503
504         try {
505             getBridgeHandler().putResource(putResource);
506         } catch (ApiException | AssetNotLoadedException e) {
507             if (logger.isDebugEnabled()) {
508                 logger.debug("{} -> handleCommand() error {}", resourceId, e.getMessage(), e);
509             } else {
510                 logger.warn("Command '{}' for thing '{}', channel '{}' failed with error '{}'.", command,
511                         thing.getUID(), channelUID, e.getMessage());
512             }
513         } catch (InterruptedException e) {
514         }
515     }
516
517     /**
518      * Handle a 'dynamics' command for the given channel ID for the given dynamics duration.
519      *
520      * @param channelId the ID of the target channel.
521      * @param command the new target state.
522      * @param duration the transition duration.
523      */
524     public synchronized void handleDynamicsCommand(String channelId, Command command, QuantityType<?> duration) {
525         if (DYNAMIC_CHANNELS.contains(channelId)) {
526             Channel dynamicsChannel = thing.getChannel(CHANNEL_2_DYNAMICS);
527             Channel targetChannel = thing.getChannel(channelId);
528             if (Objects.nonNull(dynamicsChannel) && Objects.nonNull(targetChannel)) {
529                 logger.debug("{} - handleDynamicsCommand() channelId:{}, command:{}, duration:{}", resourceId,
530                         channelId, command, duration);
531                 handleCommand(dynamicsChannel.getUID(), duration);
532                 handleCommand(targetChannel.getUID(), command);
533                 return;
534             }
535         }
536         logger.warn("Dynamics command '{}' for thing '{}', channel '{}' and duration'{}' failed.", command,
537                 thing.getUID(), channelId, duration);
538     }
539
540     @Override
541     public void initialize() {
542         Clip2ThingConfig config = getConfigAs(Clip2ThingConfig.class);
543
544         String resourceId = config.resourceId;
545         if (resourceId.isBlank()) {
546             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
547                     "@text/offline.api2.conf-error.resource-id-bad");
548             return;
549         }
550         thisResource.setId(resourceId);
551         this.resourceId = resourceId;
552         logger.debug("{} -> initialize()", resourceId);
553
554         updateThingFromLegacy();
555         updateStatus(ThingStatus.UNKNOWN);
556
557         dynamicsDuration = Duration.ZERO;
558         dynamicsExpireTime = Instant.MIN;
559
560         disposing = false;
561         hasConnectivityIssue = false;
562         updatePropertiesDone = false;
563         updateDependenciesDone = false;
564         updateLightPropertiesDone = false;
565         updateSceneContributorsDone = false;
566
567         Bridge bridge = getBridge();
568         if (Objects.nonNull(bridge)) {
569             BridgeHandler bridgeHandler = bridge.getHandler();
570             if (bridgeHandler instanceof Clip2BridgeHandler) {
571                 ((Clip2BridgeHandler) bridgeHandler).childInitialized();
572             }
573         }
574     }
575
576     /**
577      * Update the channel state depending on a new resource sent from the bridge.
578      *
579      * @param resource a Resource object containing the new state.
580      */
581     public void onResource(Resource resource) {
582         if (!disposing) {
583             boolean resourceConsumed = false;
584             String incomingResourceId = resource.getId();
585             if (resourceId.equals(incomingResourceId)) {
586                 if (resource.hasFullState()) {
587                     thisResource = resource;
588                     if (!updatePropertiesDone) {
589                         updateProperties(resource);
590                         resourceConsumed = updatePropertiesDone;
591                     }
592                 }
593                 if (!updateDependenciesDone) {
594                     resourceConsumed = true;
595                     cancelTask(updateDependenciesTask, false);
596                     updateDependenciesTask = scheduler.submit(() -> updateDependencies());
597                 }
598             } else if (ResourceType.SCENE == resource.getType()) {
599                 Resource cachedScene = sceneContributorsCache.get(incomingResourceId);
600                 if (Objects.nonNull(cachedScene)) {
601                     Setters.setResource(resource, cachedScene);
602                     resourceConsumed = updateChannels(resource);
603                     sceneContributorsCache.put(incomingResourceId, resource);
604                 }
605             } else {
606                 Resource cachedService = serviceContributorsCache.get(incomingResourceId);
607                 if (Objects.nonNull(cachedService)) {
608                     Setters.setResource(resource, cachedService);
609                     resourceConsumed = updateChannels(resource);
610                     serviceContributorsCache.put(incomingResourceId, resource);
611                     if (ResourceType.LIGHT == resource.getType() && !updateLightPropertiesDone) {
612                         updateLightProperties(resource);
613                     }
614                 }
615             }
616             if (resourceConsumed) {
617                 logger.debug("{} -> onResource() consumed resource {}", resourceId, resource);
618             }
619         }
620     }
621
622     /**
623      * Update the thing internal state depending on a full list of resources sent from the bridge. If the resourceType
624      * is SCENE then call updateScenes(), otherwise if the resource refers to this thing, consume it via onResource() as
625      * any other resource, or else if the resourceType nevertheless matches the thing type, set the thing state offline.
626      *
627      * @param resourceType the type of the resources in the list.
628      * @param fullResources the full list of resources of the given type.
629      */
630     public void onResourcesList(ResourceType resourceType, List<Resource> fullResources) {
631         if (resourceType == ResourceType.SCENE) {
632             updateSceneContributors(fullResources);
633         } else {
634             fullResources.stream().filter(r -> resourceId.equals(r.getId())).findAny()
635                     .ifPresentOrElse(r -> onResource(r), () -> {
636                         if (resourceType == thisResource.getType()) {
637                             logger.debug("{} -> onResourcesList() configuration error: unknown resourceId", resourceId);
638                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
639                                     "@text/offline.api2.conf-error.resource-id-bad");
640                         }
641                     });
642         }
643     }
644
645     /**
646      * Process the incoming Resource to initialize the alert channel.
647      *
648      * @param resource a Resource possibly with an Alerts element.
649      */
650     private void updateAlertChannel(Resource resource) {
651         Alerts alerts = resource.getAlerts();
652         if (Objects.nonNull(alerts)) {
653             List<StateOption> stateOptions = alerts.getActionValues().stream().map(action -> action.name())
654                     .map(actionId -> new StateOption(actionId, actionId)).collect(Collectors.toList());
655             if (!stateOptions.isEmpty()) {
656                 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_ALERT), stateOptions);
657                 logger.debug("{} -> updateAlerts() found {} associated alerts", resourceId, stateOptions.size());
658             }
659         }
660     }
661
662     /**
663      * If this v2 thing has a matching v1 legacy thing in the system, then for each channel in the v1 thing that
664      * corresponds to an equivalent channel in this v2 thing, and for all items that are linked to the v1 channel,
665      * create a new channel/item link between that item and the respective v2 channel in this thing.
666      */
667     private void updateChannelItemLinksFromLegacy() {
668         if (!disposing) {
669             legacyLinkedChannelUIDs.forEach(legacyLinkedChannelUID -> {
670                 String targetChannelId = REPLICATE_CHANNEL_ID_MAP.get(legacyLinkedChannelUID.getId());
671                 if (Objects.nonNull(targetChannelId)) {
672                     Channel targetChannel = thing.getChannel(targetChannelId);
673                     if (Objects.nonNull(targetChannel)) {
674                         ChannelUID uid = targetChannel.getUID();
675                         itemChannelLinkRegistry.getLinkedItems(legacyLinkedChannelUID).forEach(linkedItem -> {
676                             String item = linkedItem.getName();
677                             if (!itemChannelLinkRegistry.isLinked(item, uid)) {
678                                 if (logger.isDebugEnabled()) {
679                                     logger.debug(
680                                             "{} -> updateChannelItemLinksFromLegacy() item:{} linked to channel:{}",
681                                             resourceId, item, uid);
682                                 } else {
683                                     logger.info("Item '{}' linked to thing '{}' channel '{}'", item, thing.getUID(),
684                                             targetChannelId);
685                                 }
686                                 itemChannelLinkRegistry.add(new ItemChannelLink(item, uid));
687                             }
688                         });
689                     }
690                 }
691             });
692             legacyLinkedChannelUIDs.clear();
693         }
694     }
695
696     /**
697      * Set the active list of channels by removing any that had initially been created by the thing XML declaration, but
698      * which in fact did not have data returned from the bridge i.e. channels which are not in the supportedChannelIdSet
699      *
700      * Also warn if there are channels in the supportedChannelIdSet set which are not in the thing.
701      *
702      * Adjusts the channel list so that only the highest level channel is available in the normal channel list. If a
703      * light supports the color channel, then it's brightness and switch can be commanded via the 'B' part of the HSB
704      * channel value. And if it supports the brightness channel the switch can be controlled via the brightness. So we
705      * can remove these lower level channels from the normal channel list.
706      *
707      * For more advanced applications, it is necessary to orthogonally command the color xy parameter, dimming
708      * parameter, and/or on/off parameter independently. So we add corresponding advanced level 'CHANNEL_2_BLAH_ONLY'
709      * channels for that purpose. Since they are advanced level, normal users should normally not be confused by them,
710      * yet advanced users can use them nevertheless.
711      */
712     private void updateChannelList() {
713         if (!disposing) {
714             synchronized (supportedChannelIdSet) {
715                 logger.debug("{} -> updateChannelList()", resourceId);
716
717                 if (supportedChannelIdSet.contains(CHANNEL_2_COLOR)) {
718                     supportedChannelIdSet.add(CHANNEL_2_COLOR_XY_ONLY);
719                     //
720                     supportedChannelIdSet.remove(CHANNEL_2_BRIGHTNESS);
721                     supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
722                     //
723                     supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
724                     supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
725                 }
726                 if (supportedChannelIdSet.contains(CHANNEL_2_BRIGHTNESS)) {
727                     supportedChannelIdSet.add(CHANNEL_2_DIMMING_ONLY);
728                     //
729                     supportedChannelIdSet.remove(CHANNEL_2_SWITCH);
730                     supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
731                 }
732                 if (supportedChannelIdSet.contains(CHANNEL_2_SWITCH)) {
733                     supportedChannelIdSet.add(CHANNEL_2_ON_OFF_ONLY);
734                 }
735
736                 /*
737                  * This binding creates its dynamic list of channels by a 'subtractive' method i.e. the full set of
738                  * channels is initially created from the thing type xml, and then for any channels where UndfType.NULL
739                  * data is returned, the respective channel is removed from the full list. However in seldom cases
740                  * UndfType.NULL may wrongly be returned, so we should log a warning here just in case.
741                  */
742                 if (logger.isDebugEnabled()) {
743                     supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
744                             .forEach(channelId -> logger.debug(
745                                     "{} -> updateChannelList() required channel '{}' missing", resourceId, channelId));
746                 } else {
747                     supportedChannelIdSet.stream().filter(channelId -> Objects.isNull(thing.getChannel(channelId)))
748                             .forEach(channelId -> logger.warn(
749                                     "Thing '{}' is missing required channel '{}'. Please recreate the thing!",
750                                     thing.getUID(), channelId));
751                 }
752
753                 // get list of unused channels
754                 List<Channel> unusedChannels = thing.getChannels().stream()
755                         .filter(channel -> !supportedChannelIdSet.contains(channel.getUID().getId()))
756                         .collect(Collectors.toList());
757
758                 // remove any unused channels
759                 if (!unusedChannels.isEmpty()) {
760                     if (logger.isDebugEnabled()) {
761                         unusedChannels.stream().map(channel -> channel.getUID().getId())
762                                 .forEach(channelId -> logger.debug(
763                                         "{} -> updateChannelList() removing unused channel '{}'", resourceId,
764                                         channelId));
765                     }
766                     updateThing(editThing().withoutChannels(unusedChannels).build());
767                 }
768             }
769         }
770     }
771
772     /**
773      * Update the state of the existing channels.
774      *
775      * @param resource the Resource containing the new channel state.
776      * @return true if the channel was found and updated.
777      */
778     private boolean updateChannels(Resource resource) {
779         logger.debug("{} -> updateChannels() from resource {}", resourceId, resource);
780         boolean fullUpdate = resource.hasFullState();
781         switch (resource.getType()) {
782             case BUTTON:
783                 if (fullUpdate) {
784                     addSupportedChannel(CHANNEL_2_BUTTON_LAST_EVENT);
785                     controlIds.put(resource.getId(), resource.getControlId());
786                 } else {
787                     State buttonState = resource.getButtonEventState(controlIds);
788                     updateState(CHANNEL_2_BUTTON_LAST_EVENT, buttonState, fullUpdate);
789                 }
790                 break;
791
792             case DEVICE_POWER:
793                 updateState(CHANNEL_2_BATTERY_LEVEL, resource.getBatteryLevelState(), fullUpdate);
794                 updateState(CHANNEL_2_BATTERY_LOW, resource.getBatteryLowState(), fullUpdate);
795                 break;
796
797             case LIGHT:
798                 if (fullUpdate) {
799                     updateEffectChannel(resource);
800                 }
801                 updateState(CHANNEL_2_COLOR_TEMP_PERCENT, resource.getColorTemperaturePercentState(), fullUpdate);
802                 updateState(CHANNEL_2_COLOR_TEMP_ABSOLUTE, resource.getColorTemperatureAbsoluteState(), fullUpdate);
803                 updateState(CHANNEL_2_COLOR, resource.getColorState(), fullUpdate);
804                 updateState(CHANNEL_2_COLOR_XY_ONLY, resource.getColorXyState(), fullUpdate);
805                 updateState(CHANNEL_2_EFFECT, resource.getEffectState(), fullUpdate);
806                 // fall through for dimming and on/off related channels
807
808             case GROUPED_LIGHT:
809                 if (fullUpdate) {
810                     updateAlertChannel(resource);
811                 }
812                 updateState(CHANNEL_2_BRIGHTNESS, resource.getBrightnessState(), fullUpdate);
813                 updateState(CHANNEL_2_DIMMING_ONLY, resource.getDimmingState(), fullUpdate);
814                 updateState(CHANNEL_2_SWITCH, resource.getOnOffState(), fullUpdate);
815                 updateState(CHANNEL_2_ON_OFF_ONLY, resource.getOnOffState(), fullUpdate);
816                 updateState(CHANNEL_2_ALERT, resource.getAlertState(), fullUpdate);
817                 break;
818
819             case LIGHT_LEVEL:
820                 updateState(CHANNEL_2_LIGHT_LEVEL, resource.getLightLevelState(), fullUpdate);
821                 updateState(CHANNEL_2_LIGHT_LEVEL_ENABLED, resource.getEnabledState(), fullUpdate);
822                 break;
823
824             case MOTION:
825                 updateState(CHANNEL_2_MOTION, resource.getMotionState(), fullUpdate);
826                 updateState(CHANNEL_2_MOTION_ENABLED, resource.getEnabledState(), fullUpdate);
827                 break;
828
829             case RELATIVE_ROTARY:
830                 if (fullUpdate) {
831                     addSupportedChannel(CHANNEL_2_ROTARY_STEPS);
832                 } else {
833                     updateState(CHANNEL_2_ROTARY_STEPS, resource.getRotaryStepsState(), fullUpdate);
834                 }
835                 break;
836
837             case TEMPERATURE:
838                 updateState(CHANNEL_2_TEMPERATURE, resource.getTemperatureState(), fullUpdate);
839                 updateState(CHANNEL_2_TEMPERATURE_ENABLED, resource.getEnabledState(), fullUpdate);
840                 break;
841
842             case ZIGBEE_CONNECTIVITY:
843                 updateConnectivityState(resource);
844                 break;
845
846             case SCENE:
847                 updateState(CHANNEL_2_SCENE, resource.getSceneState(), fullUpdate);
848                 break;
849
850             default:
851                 return false;
852         }
853         if (thisResource.getType() == ResourceType.DEVICE) {
854             updateState(CHANNEL_2_LAST_UPDATED, new DateTimeType(), fullUpdate);
855         }
856         return true;
857     }
858
859     /**
860      * Check the Zigbee connectivity and set the thing online status accordingly. If the thing is offline then set all
861      * its channel states to undefined, otherwise execute a refresh command to update channels to the latest current
862      * state.
863      *
864      * @param resource a Resource that potentially contains the Zigbee connectivity state.
865      */
866     private void updateConnectivityState(Resource resource) {
867         ZigbeeStatus zigbeeStatus = resource.getZigbeeStatus();
868         if (Objects.nonNull(zigbeeStatus)) {
869             logger.debug("{} -> updateConnectivityState() thingStatus:{}, zigbeeStatus:{}", resourceId,
870                     thing.getStatus(), zigbeeStatus);
871             hasConnectivityIssue = zigbeeStatus != ZigbeeStatus.CONNECTED;
872             if (hasConnectivityIssue) {
873                 if (thing.getStatusInfo().getStatusDetail() != ThingStatusDetail.COMMUNICATION_ERROR) {
874                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
875                             "@text/offline.api2.comm-error.zigbee-connectivity-issue");
876                     supportedChannelIdSet.forEach(channelId -> updateState(channelId, UnDefType.UNDEF));
877                 }
878             } else if (thing.getStatus() != ThingStatus.ONLINE) {
879                 updateStatus(ThingStatus.ONLINE);
880                 // issue REFRESH command to update all channels
881                 Channel lastUpdateChannel = thing.getChannel(CHANNEL_2_LAST_UPDATED);
882                 if (Objects.nonNull(lastUpdateChannel)) {
883                     handleCommand(lastUpdateChannel.getUID(), RefreshType.REFRESH);
884                 }
885             }
886         }
887     }
888
889     /**
890      * Get all resources needed for building the thing state. Build the forward / reverse contributor lookup maps. Set
891      * up the final list of channels in the thing.
892      */
893     private synchronized void updateDependencies() {
894         if (!disposing && !updateDependenciesDone) {
895             logger.debug("{} -> updateDependencies()", resourceId);
896             try {
897                 if (!updatePropertiesDone) {
898                     logger.debug("{} -> updateDependencies() properties not initialized", resourceId);
899                     return;
900                 }
901                 if (!updateSceneContributorsDone && !updateSceneContributors()) {
902                     logger.debug("{} -> updateDependencies() scenes not initialized", resourceId);
903                     return;
904                 }
905                 updateLookups();
906                 updateServiceContributors();
907                 updateChannelList();
908                 updateChannelItemLinksFromLegacy();
909                 if (!hasConnectivityIssue) {
910                     updateStatus(ThingStatus.ONLINE);
911                 }
912                 updateDependenciesDone = true;
913             } catch (ApiException e) {
914                 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
915                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
916             } catch (AssetNotLoadedException e) {
917                 logger.debug("{} -> updateDependencies() {}", resourceId, e.getMessage(), e);
918                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
919                         "@text/offline.api2.conf-error.assets-not-loaded");
920             } catch (InterruptedException e) {
921             }
922         }
923     }
924
925     /**
926      * Process the incoming Resource to initialize the effects channel.
927      *
928      * @param resource a Resource possibly with an Effects element.
929      */
930     public void updateEffectChannel(Resource resource) {
931         Effects effects = resource.getEffects();
932         if (Objects.nonNull(effects)) {
933             List<StateOption> stateOptions = effects.getStatusValues().stream()
934                     .map(effect -> EffectType.of(effect).name()).map(effectId -> new StateOption(effectId, effectId))
935                     .collect(Collectors.toList());
936             if (!stateOptions.isEmpty()) {
937                 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_EFFECT),
938                         stateOptions);
939                 logger.debug("{} -> updateEffects() found {} effects", resourceId, stateOptions.size());
940             }
941         }
942     }
943
944     /**
945      * Update the light properties.
946      *
947      * @param resource a Resource object containing the property data.
948      */
949     private synchronized void updateLightProperties(Resource resource) {
950         if (!disposing && !updateLightPropertiesDone) {
951             logger.debug("{} -> updateLightProperties()", resourceId);
952
953             Dimming dimming = resource.getDimming();
954             thing.setProperty(PROPERTY_DIMMING_RANGE, Objects.nonNull(dimming) ? dimming.toPropertyValue() : null);
955
956             MirekSchema mirekSchema = resource.getMirekSchema();
957             thing.setProperty(PROPERTY_COLOR_TEMP_RANGE,
958                     Objects.nonNull(mirekSchema) ? mirekSchema.toPropertyValue() : null);
959
960             ColorXy colorXy = resource.getColorXy();
961             Gamut2 gamut = Objects.nonNull(colorXy) ? colorXy.getGamut2() : null;
962             thing.setProperty(PROPERTY_COLOR_GAMUT, Objects.nonNull(gamut) ? gamut.toPropertyValue() : null);
963
964             updateLightPropertiesDone = true;
965         }
966     }
967
968     /**
969      * Initialize the lookup maps of resources that contribute to the thing state.
970      */
971     private void updateLookups() {
972         if (!disposing) {
973             logger.debug("{} -> updateLookups()", resourceId);
974             // get supported services
975             List<ResourceReference> services = thisResource.getServiceReferences();
976
977             // add supported services to contributorsCache
978             serviceContributorsCache.clear();
979             serviceContributorsCache.putAll(services.stream()
980                     .collect(Collectors.toMap(ResourceReference::getId, r -> new Resource(r.getType()))));
981
982             // add supported services to commandResourceIds
983             commandResourceIds.clear();
984             commandResourceIds.putAll(services.stream() // use a 'mergeFunction' to prevent duplicates
985                     .collect(Collectors.toMap(ResourceReference::getType, ResourceReference::getId, (r1, r2) -> r1)));
986         }
987     }
988
989     /**
990      * Update the primary device properties.
991      *
992      * @param resource a Resource object containing the property data.
993      */
994     private synchronized void updateProperties(Resource resource) {
995         if (!disposing && !updatePropertiesDone) {
996             logger.debug("{} -> updateProperties()", resourceId);
997             Map<String, String> properties = new HashMap<>(thing.getProperties());
998
999             // resource data
1000             properties.put(PROPERTY_RESOURCE_TYPE, thisResource.getType().toString());
1001             properties.put(PROPERTY_RESOURCE_NAME, thisResource.getName());
1002
1003             // owner information
1004             ResourceReference owner = thisResource.getOwner();
1005             if (Objects.nonNull(owner)) {
1006                 String ownerId = owner.getId();
1007                 if (Objects.nonNull(ownerId)) {
1008                     properties.put(PROPERTY_OWNER, ownerId);
1009                 }
1010                 ResourceType ownerType = owner.getType();
1011                 properties.put(PROPERTY_OWNER_TYPE, ownerType.toString());
1012             }
1013
1014             // metadata
1015             MetaData metaData = thisResource.getMetaData();
1016             if (Objects.nonNull(metaData)) {
1017                 properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString());
1018             }
1019
1020             // product data
1021             ProductData productData = thisResource.getProductData();
1022             if (Objects.nonNull(productData)) {
1023                 // standard properties
1024                 properties.put(PROPERTY_RESOURCE_ID, resourceId);
1025                 properties.put(Thing.PROPERTY_MODEL_ID, productData.getModelId());
1026                 properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName());
1027                 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion());
1028                 String hardwarePlatformType = productData.getHardwarePlatformType();
1029                 if (Objects.nonNull(hardwarePlatformType)) {
1030                     properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType);
1031                 }
1032
1033                 // hue specific properties
1034                 properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName());
1035                 properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString());
1036                 properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString());
1037             }
1038
1039             thing.setProperties(properties);
1040             updatePropertiesDone = true;
1041         }
1042     }
1043
1044     /**
1045      * Execute an HTTP GET command to fetch the resources data for the referenced resource.
1046      *
1047      * @param reference to the required resource.
1048      * @throws ApiException if a communication error occurred.
1049      * @throws AssetNotLoadedException if one of the assets is not loaded.
1050      * @throws InterruptedException
1051      */
1052     private void updateResource(ResourceReference reference)
1053             throws ApiException, AssetNotLoadedException, InterruptedException {
1054         if (!disposing) {
1055             logger.debug("{} -> updateResource() from resource {}", resourceId, reference);
1056             getBridgeHandler().getResources(reference).getResources().stream()
1057                     .forEach(resource -> onResource(resource));
1058         }
1059     }
1060
1061     /**
1062      * Fetch the full list of scenes from the bridge, and call updateSceneContributors(List<Resource> allScenes)
1063      *
1064      * @throws ApiException if a communication error occurred.
1065      * @throws AssetNotLoadedException if one of the assets is not loaded.
1066      * @throws InterruptedException
1067      */
1068     public boolean updateSceneContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1069         if (!disposing && !updateSceneContributorsDone) {
1070             ResourceReference scenesReference = new ResourceReference().setType(ResourceType.SCENE);
1071             updateSceneContributors(getBridgeHandler().getResources(scenesReference).getResources());
1072         }
1073         return updateSceneContributorsDone;
1074     }
1075
1076     /**
1077      * Process the incoming list of scene resources to find those scenes which contribute to this thing. And if there
1078      * are any, include a scene channel in the supported channel list, and populate its respective state options.
1079      *
1080      * @param allScenes the full list of scene resources.
1081      */
1082     public synchronized boolean updateSceneContributors(List<Resource> allScenes) {
1083         if (!disposing && !updateSceneContributorsDone) {
1084             sceneContributorsCache.clear();
1085             sceneResourceIds.clear();
1086
1087             ResourceReference thisReference = getResourceReference();
1088             List<Resource> scenes = allScenes.stream().filter(s -> thisReference.equals(s.getGroup()))
1089                     .collect(Collectors.toList());
1090
1091             if (!scenes.isEmpty()) {
1092                 sceneContributorsCache.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)));
1093                 sceneResourceIds.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s.getId())));
1094
1095                 State state = scenes.stream().filter(s -> s.getSceneActive().orElse(false)).map(s -> s.getSceneState())
1096                         .findAny().orElse(UnDefType.UNDEF);
1097                 updateState(CHANNEL_2_SCENE, state, true);
1098
1099                 stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_SCENE), scenes
1100                         .stream().map(s -> s.getName()).map(n -> new StateOption(n, n)).collect(Collectors.toList()));
1101
1102                 logger.debug("{} -> updateSceneContributors() found {} scenes", resourceId, scenes.size());
1103             }
1104             updateSceneContributorsDone = true;
1105         }
1106         return updateSceneContributorsDone;
1107     }
1108
1109     /**
1110      * Execute a series of HTTP GET commands to fetch the resource data for all service resources that contribute to the
1111      * thing state.
1112      *
1113      * @throws ApiException if a communication error occurred.
1114      * @throws AssetNotLoadedException if one of the assets is not loaded.
1115      * @throws InterruptedException
1116      */
1117     private void updateServiceContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
1118         if (!disposing) {
1119             logger.debug("{} -> updateServiceContributors() called for {} contributors", resourceId,
1120                     serviceContributorsCache.size());
1121             ResourceReference reference = new ResourceReference();
1122             for (var entry : serviceContributorsCache.entrySet()) {
1123                 updateResource(reference.setId(entry.getKey()).setType(entry.getValue().getType()));
1124             }
1125         }
1126     }
1127
1128     /**
1129      * Update the channel state, and if appropriate add the channel ID to the set of supportedChannelIds. Calls either
1130      * OH core updateState() or triggerChannel() methods depending on the channel kind.
1131      *
1132      * Note: the particular 'UnDefType.UNDEF' value of the state argument is used to specially indicate the undefined
1133      * state, but yet that its channel shall nevertheless continue to be present in the thing.
1134      *
1135      * @param channelID the id of the channel.
1136      * @param state the new state of the channel.
1137      * @param fullUpdate if true always update the channel, otherwise only update if state is not 'UNDEF'.
1138      */
1139     private void updateState(String channelID, State state, boolean fullUpdate) {
1140         boolean isDefined = state != UnDefType.NULL;
1141         Channel channel = thing.getChannel(channelID);
1142
1143         if ((fullUpdate || isDefined) && Objects.nonNull(channel)) {
1144             logger.debug("{} -> updateState() '{}' update with '{}' (fullUpdate:{}, isDefined:{})", resourceId,
1145                     channelID, state, fullUpdate, isDefined);
1146
1147             switch (channel.getKind()) {
1148                 case STATE:
1149                     updateState(channelID, state);
1150                     break;
1151
1152                 case TRIGGER:
1153                     if (state instanceof DecimalType) {
1154                         triggerChannel(channelID, String.valueOf(((DecimalType) state).intValue()));
1155                     }
1156             }
1157         }
1158         if (fullUpdate && isDefined) {
1159             addSupportedChannel(channelID);
1160         }
1161     }
1162
1163     /**
1164      * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's
1165      * settings into this thing.
1166      */
1167     private void updateThingFromLegacy() {
1168         if (isInitialized()) {
1169             logger.warn("Cannot update thing '{}' from legacy thing since handler already initialized.",
1170                     thing.getUID());
1171             return;
1172         }
1173         Map<String, String> properties = thing.getProperties();
1174         String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID);
1175         if (Objects.nonNull(legacyThingUID)) {
1176             Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID));
1177             if (Objects.nonNull(legacyThing)) {
1178                 ThingBuilder editBuilder = editThing();
1179
1180                 String location = legacyThing.getLocation();
1181                 if (Objects.nonNull(location) && !location.isBlank()) {
1182                     editBuilder = editBuilder.withLocation(location);
1183                 }
1184
1185                 // save list of legacyLinkedChannelUIDs for use after channel list is initialised
1186                 legacyLinkedChannelUIDs.clear();
1187                 legacyLinkedChannelUIDs.addAll(legacyThing.getChannels().stream().map(Channel::getUID)
1188                         .filter(uid -> REPLICATE_CHANNEL_ID_MAP.containsKey(uid.getId())
1189                                 && itemChannelLinkRegistry.isLinked(uid))
1190                         .collect(Collectors.toList()));
1191
1192                 Map<String, String> newProperties = new HashMap<>(properties);
1193                 newProperties.remove(PROPERTY_LEGACY_THING_UID);
1194
1195                 updateThing(editBuilder.withProperties(newProperties).build());
1196             }
1197         }
1198     }
1199 }