2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hue.internal.handler;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.Objects;
23 import java.util.Optional;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.Future;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.hue.internal.api.dto.clip2.MetaData;
34 import org.openhab.binding.hue.internal.api.dto.clip2.ProductData;
35 import org.openhab.binding.hue.internal.api.dto.clip2.Resource;
36 import org.openhab.binding.hue.internal.api.dto.clip2.ResourceReference;
37 import org.openhab.binding.hue.internal.api.dto.clip2.Resources;
38 import org.openhab.binding.hue.internal.api.dto.clip2.enums.Archetype;
39 import org.openhab.binding.hue.internal.api.dto.clip2.enums.ResourceType;
40 import org.openhab.binding.hue.internal.api.dto.clip2.helper.Setters;
41 import org.openhab.binding.hue.internal.config.Clip2BridgeConfig;
42 import org.openhab.binding.hue.internal.connection.Clip2Bridge;
43 import org.openhab.binding.hue.internal.connection.HueTlsTrustManagerProvider;
44 import org.openhab.binding.hue.internal.discovery.Clip2ThingDiscoveryService;
45 import org.openhab.binding.hue.internal.exceptions.ApiException;
46 import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException;
47 import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.i18n.LocaleProvider;
50 import org.openhab.core.i18n.TranslationProvider;
51 import org.openhab.core.io.net.http.HttpClientFactory;
52 import org.openhab.core.io.net.http.TlsTrustManagerProvider;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingRegistry;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.ThingTypeUID;
60 import org.openhab.core.thing.ThingUID;
61 import org.openhab.core.thing.binding.BaseBridgeHandler;
62 import org.openhab.core.thing.binding.ThingHandler;
63 import org.openhab.core.thing.binding.ThingHandlerService;
64 import org.openhab.core.thing.binding.builder.BridgeBuilder;
65 import org.openhab.core.types.Command;
66 import org.openhab.core.types.RefreshType;
67 import org.osgi.framework.Bundle;
68 import org.osgi.framework.FrameworkUtil;
69 import org.osgi.framework.ServiceRegistration;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
74 * Bridge handler for a CLIP 2 bridge. It communicates with the bridge via CLIP 2 end points, and reads and writes API
75 * V2 resource objects. It also subscribes to the server's SSE event stream, and receives SSE events from it.
77 * @author Andrew Fiddian-Green - Initial contribution.
80 public class Clip2BridgeHandler extends BaseBridgeHandler {
82 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE_API2);
84 private static final int FAST_SCHEDULE_MILLI_SECONDS = 500;
85 private static final int APPLICATION_KEY_MAX_TRIES = 600; // i.e. 300 seconds, 5 minutes
86 private static final int RECONNECT_DELAY_SECONDS = 10;
87 private static final int RECONNECT_MAX_TRIES = 5;
89 private static final ResourceReference DEVICE = new ResourceReference().setType(ResourceType.DEVICE);
90 private static final ResourceReference ROOM = new ResourceReference().setType(ResourceType.ROOM);
91 private static final ResourceReference ZONE = new ResourceReference().setType(ResourceType.ZONE);
92 private static final ResourceReference BRIDGE = new ResourceReference().setType(ResourceType.BRIDGE);
93 private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME);
94 private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE);
95 private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE);
98 * List of resource references that need to be mass down loaded.
99 * NOTE: the SCENE resources must be mass down loaded first!
101 private static final List<ResourceReference> MASS_DOWNLOAD_RESOURCE_REFERENCES = List.of(SCENE, DEVICE, ROOM, ZONE);
103 private final Logger logger = LoggerFactory.getLogger(Clip2BridgeHandler.class);
105 private final HttpClientFactory httpClientFactory;
106 private final ThingRegistry thingRegistry;
107 private final Bundle bundle;
108 private final LocaleProvider localeProvider;
109 private final TranslationProvider translationProvider;
111 private @Nullable Clip2Bridge clip2Bridge;
112 private @Nullable ServiceRegistration<?> trustManagerRegistration;
113 private @Nullable Clip2ThingDiscoveryService discoveryService;
115 private @Nullable Future<?> checkConnectionTask;
116 private @Nullable Future<?> updateOnlineStateTask;
117 private @Nullable ScheduledFuture<?> scheduledUpdateTask;
118 private Map<Integer, Future<?>> resourcesEventTasks = new ConcurrentHashMap<>();
120 private boolean assetsLoaded;
121 private int applKeyRetriesRemaining;
122 private int connectRetriesRemaining;
124 public Clip2BridgeHandler(Bridge bridge, HttpClientFactory httpClientFactory, ThingRegistry thingRegistry,
125 LocaleProvider localeProvider, TranslationProvider translationProvider) {
127 this.httpClientFactory = httpClientFactory;
128 this.thingRegistry = thingRegistry;
129 this.bundle = FrameworkUtil.getBundle(getClass());
130 this.localeProvider = localeProvider;
131 this.translationProvider = translationProvider;
135 * Cancel the given task.
137 * @param cancelTask the task to be cancelled (may be null)
138 * @param mayInterrupt allows cancel() to interrupt the thread.
140 private void cancelTask(@Nullable Future<?> cancelTask, boolean mayInterrupt) {
141 if (Objects.nonNull(cancelTask)) {
142 cancelTask.cancel(mayInterrupt);
147 * Check if assets are loaded.
149 * @throws AssetNotLoadedException if assets not loaded.
151 private void checkAssetsLoaded() throws AssetNotLoadedException {
153 throw new AssetNotLoadedException("Assets not loaded");
158 * Try to connect and set the online status accordingly. If the connection attempt throws an
159 * HttpUnAuthorizedException then try to register the existing application key, or create a new one, with the hub.
160 * If the connection attempt throws an ApiException then set the thing status to offline. This method is called on a
161 * scheduler thread, which reschedules itself repeatedly until the thing is shutdown.
163 private synchronized void checkConnection() {
164 logger.debug("checkConnection()");
166 boolean retryApplicationKey = false;
167 boolean retryConnection = false;
171 getClip2Bridge().testConnectionState();
172 updateSelf(); // go online
173 } catch (HttpUnauthorizedException unauthorizedException) {
174 logger.debug("checkConnection() {}", unauthorizedException.getMessage(), unauthorizedException);
175 if (applKeyRetriesRemaining > 0) {
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
177 "@text/offline.api2.conf-error.press-pairing-button");
179 registerApplicationKey();
180 retryApplicationKey = true;
181 } catch (HttpUnauthorizedException e) {
182 retryApplicationKey = true;
183 } catch (ApiException e) {
184 setStatusOfflineWithCommunicationError(e);
185 } catch (IllegalStateException e) {
186 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
187 "@text/offline.api2.conf-error.read-only");
188 } catch (AssetNotLoadedException e) {
189 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
190 "@text/offline.api2.conf-error.assets-not-loaded");
191 } catch (InterruptedException e) {
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
196 "@text/offline.api2.conf-error.not-authorized");
198 } catch (ApiException e) {
199 logger.debug("checkConnection() {}", e.getMessage(), e);
200 setStatusOfflineWithCommunicationError(e);
201 retryConnection = connectRetriesRemaining > 0;
202 } catch (AssetNotLoadedException e) {
203 logger.debug("checkConnection() {}", e.getMessage(), e);
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205 "@text/offline.api2.conf-error.assets-not-loaded");
206 } catch (InterruptedException e) {
211 if (retryApplicationKey) {
212 // short delay used during attempts to create or validate an application key
213 milliSeconds = FAST_SCHEDULE_MILLI_SECONDS;
214 applKeyRetriesRemaining--;
216 // default delay, set via configuration parameter, used as heart-beat 'just-in-case'
217 Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class);
218 milliSeconds = config.checkMinutes * 60000;
219 if (retryConnection) {
220 // exponential back off delay used during attempts to reconnect
221 int backOffDelay = 60000 * (int) Math.pow(2, RECONNECT_MAX_TRIES - connectRetriesRemaining);
222 milliSeconds = Math.min(milliSeconds, backOffDelay);
223 connectRetriesRemaining--;
227 // this method schedules itself to be called again in a loop..
228 cancelTask(checkConnectionTask, false);
229 checkConnectionTask = scheduler.schedule(() -> checkConnection(), milliSeconds, TimeUnit.MILLISECONDS);
232 private void setStatusOfflineWithCommunicationError(Exception e) {
233 Throwable cause = e.getCause();
234 String causeMessage = cause == null ? null : cause.getMessage();
235 if (causeMessage == null || causeMessage.isEmpty()) {
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
237 "@text/offline.api2.comm-error.exception [\"" + e.getMessage() + "\"]");
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
240 "@text/offline.api2.comm-error.exception [\"" + e.getMessage() + " -> " + causeMessage + "\"]");
245 * If a child thing has been added, and the bridge is online, update the child's data.
247 public void childInitialized() {
248 if (thing.getStatus() == ThingStatus.ONLINE) {
249 updateThingsScheduled(5000);
254 public void dispose() {
261 * Dispose the bridge handler's assets. Called from dispose() on a thread, so that dispose() itself can complete
264 private void disposeAssets() {
265 logger.debug("disposeAssets() {}", this);
266 synchronized (this) {
267 assetsLoaded = false;
268 cancelTask(checkConnectionTask, true);
269 cancelTask(updateOnlineStateTask, true);
270 cancelTask(scheduledUpdateTask, true);
271 checkConnectionTask = null;
272 updateOnlineStateTask = null;
273 scheduledUpdateTask = null;
274 synchronized (resourcesEventTasks) {
275 resourcesEventTasks.values().forEach(task -> cancelTask(task, true));
276 resourcesEventTasks.clear();
278 ServiceRegistration<?> registration = trustManagerRegistration;
279 if (Objects.nonNull(registration)) {
280 registration.unregister();
281 trustManagerRegistration = null;
283 Clip2Bridge bridge = clip2Bridge;
284 if (Objects.nonNull(bridge)) {
288 Clip2ThingDiscoveryService disco = discoveryService;
289 if (Objects.nonNull(disco)) {
296 * Return the application key for the console app.
298 * @return the application key.
300 public String getApplicationKey() {
301 Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class);
302 return config.applicationKey;
306 * Get the Clip2Bridge connection and throw an exception if it is null.
308 * @return the Clip2Bridge.
309 * @throws AssetNotLoadedException if the Clip2Bridge is null.
311 private Clip2Bridge getClip2Bridge() throws AssetNotLoadedException {
312 Clip2Bridge clip2Bridge = this.clip2Bridge;
313 if (Objects.nonNull(clip2Bridge)) {
316 throw new AssetNotLoadedException("Clip2Bridge is null");
320 * Return the IP address for the console app.
322 * @return the IP address.
324 public String getIpAddress() {
325 Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class);
326 return config.ipAddress;
330 * Get the v1 legacy Hue bridge (if any) which has the same IP address as this.
332 * @return Optional result containing the legacy bridge (if any found).
334 public Optional<Thing> getLegacyBridge() {
335 String ipAddress = getIpAddress();
336 return Objects.nonNull(ipAddress)
337 ? thingRegistry.getAll().stream()
338 .filter(thing -> thing.getThingTypeUID().equals(THING_TYPE_BRIDGE)
339 && ipAddress.equals(thing.getConfiguration().get("ipAddress")))
345 * Get the v1 legacy Hue thing (if any) which has a Bridge having the same IP address as this, and an ID that
346 * matches the given parameter.
348 * @param targetIdV1 the idV1 attribute to match.
349 * @return Optional result containing the legacy thing (if found).
351 public Optional<Thing> getLegacyThing(String targetIdV1) {
352 Optional<Thing> legacyBridge = getLegacyBridge();
353 if (legacyBridge.isEmpty()) {
354 return Optional.empty();
358 if (targetIdV1.startsWith("/lights/")) {
360 } else if (targetIdV1.startsWith("/sensors/")) {
362 } else if (targetIdV1.startsWith("/groups/")) {
365 return Optional.empty();
368 ThingUID legacyBridgeUID = legacyBridge.get().getUID();
369 return thingRegistry.getAll().stream() //
370 .filter(thing -> legacyBridgeUID.equals(thing.getBridgeUID())
371 && V1_THING_TYPE_UIDS.contains(thing.getThingTypeUID())) //
373 Object id = thing.getConfiguration().get(config);
374 return (id instanceof String) && targetIdV1.endsWith("/" + (String) id);
379 * Return a localized text.
381 * @param key the i18n text key.
382 * @param arguments for parameterized translation.
383 * @return the localized text.
385 public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) {
386 String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
387 return Objects.nonNull(result) ? result : key;
391 * Execute an HTTP GET for a resources reference object from the server.
393 * @param reference containing the resourceType and (optionally) the resourceId of the resource to get. If the
394 * resourceId is null then all resources of the given type are returned.
395 * @return the resource, or null if something fails.
396 * @throws ApiException if a communication error occurred.
397 * @throws AssetNotLoadedException if one of the assets is not loaded.
398 * @throws InterruptedException
400 public Resources getResources(ResourceReference reference)
401 throws ApiException, AssetNotLoadedException, InterruptedException {
402 logger.debug("getResources() {}", reference);
404 return getClip2Bridge().getResources(reference);
408 * Getter for the scheduler.
410 * @return the scheduler.
412 public ScheduledExecutorService getScheduler() {
417 public Collection<Class<? extends ThingHandlerService>> getServices() {
418 return Set.of(Clip2ThingDiscoveryService.class);
422 public void handleCommand(ChannelUID channelUID, Command command) {
423 if (RefreshType.REFRESH.equals(command)) {
426 logger.warn("Bridge thing '{}' has no channels, only REFRESH command supported.", thing.getUID());
430 public void initialize() {
431 updateThingFromLegacy();
432 updateStatus(ThingStatus.UNKNOWN);
433 applKeyRetriesRemaining = APPLICATION_KEY_MAX_TRIES;
434 connectRetriesRemaining = RECONNECT_MAX_TRIES;
439 * Initialize the bridge handler's assets.
441 private void initializeAssets() {
442 logger.debug("initializeAssets() {}", this);
443 synchronized (this) {
444 Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class);
446 String ipAddress = config.ipAddress;
447 if (ipAddress.isBlank()) {
448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
449 "@text/offline.conf-error-no-ip-address");
454 if (!Clip2Bridge.isClip2Supported(ipAddress)) {
455 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
456 "@text/offline.api2.conf-error.clip2-not-supported");
459 } catch (IOException e) {
460 logger.trace("initializeAssets() communication error on '{}'", ipAddress, e);
461 setStatusOfflineWithCommunicationError(e);
465 HueTlsTrustManagerProvider trustManagerProvider = new HueTlsTrustManagerProvider(ipAddress + ":443",
466 config.useSelfSignedCertificate);
468 if (Objects.isNull(trustManagerProvider.getPEMTrustManager())) {
469 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
470 "@text/offline.api2.conf-error.certificate-load");
474 trustManagerRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext()
475 .registerService(TlsTrustManagerProvider.class.getName(), trustManagerProvider, null);
477 String applicationKey = config.applicationKey;
478 applicationKey = Objects.nonNull(applicationKey) ? applicationKey : "";
481 clip2Bridge = new Clip2Bridge(httpClientFactory, this, ipAddress, applicationKey);
482 } catch (ApiException e) {
483 logger.trace("initializeAssets() communication error on '{}'", ipAddress, e);
484 setStatusOfflineWithCommunicationError(e);
490 cancelTask(checkConnectionTask, false);
491 checkConnectionTask = scheduler.submit(() -> checkConnection());
495 * Called when the connection goes offline. Schedule a reconnection.
497 public void onConnectionOffline() {
499 cancelTask(checkConnectionTask, false);
500 checkConnectionTask = scheduler.schedule(() -> checkConnection(), RECONNECT_DELAY_SECONDS,
506 * Called when the connection goes online. Schedule a general state update.
508 public void onConnectionOnline() {
509 cancelTask(updateOnlineStateTask, false);
510 updateOnlineStateTask = scheduler.schedule(() -> updateOnlineState(), 0, TimeUnit.MILLISECONDS);
514 * Called when an SSE event message comes in with a valid list of resources. For each resource received, inform all
515 * child thing handlers with the respective resource.
517 * @param resources a list of incoming resource objects.
519 public void onResourcesEvent(List<Resource> resources) {
521 synchronized (resourcesEventTasks) {
522 int index = resourcesEventTasks.size();
523 resourcesEventTasks.put(index, scheduler.submit(() -> {
524 onResourcesEventTask(resources);
525 resourcesEventTasks.remove(index);
531 private void onResourcesEventTask(List<Resource> resources) {
532 int numberOfResources = resources.size();
533 logger.debug("onResourcesEventTask() resource count {}", numberOfResources);
534 Setters.mergeLightResources(resources);
535 if (numberOfResources != resources.size()) {
536 logger.debug("onResourcesEventTask() merged to {} resources", resources.size());
538 getThing().getThings().forEach(thing -> {
539 if (thing.getHandler() instanceof Clip2ThingHandler clip2ThingHandler) {
540 clip2ThingHandler.onResources(resources);
546 * Execute an HTTP PUT to send a Resource object to the server.
548 * @param resource the resource to put.
549 * @return the resource, which may contain errors.
550 * @throws ApiException if a communication error occurred.
551 * @throws AssetNotLoadedException if one of the assets is not loaded.
552 * @throws InterruptedException
554 public Resources putResource(Resource resource) throws ApiException, AssetNotLoadedException, InterruptedException {
555 logger.debug("putResource() {}", resource);
557 return getClip2Bridge().putResource(resource);
561 * Register the application key with the hub. If the current application key is empty it will create a new one.
563 * @throws HttpUnauthorizedException if the communication was OK but the registration failed anyway.
564 * @throws ApiException if a communication error occurred.
565 * @throws AssetNotLoadedException if one of the assets is not loaded.
566 * @throws IllegalStateException if the configuration cannot be changed e.g. read only.
567 * @throws InterruptedException
569 private void registerApplicationKey() throws HttpUnauthorizedException, ApiException, AssetNotLoadedException,
570 IllegalStateException, InterruptedException {
571 logger.debug("registerApplicationKey()");
572 Clip2BridgeConfig config = getConfigAs(Clip2BridgeConfig.class);
573 String newApplicationKey = getClip2Bridge().registerApplicationKey(config.applicationKey);
574 Configuration configuration = editConfiguration();
575 configuration.put(Clip2BridgeConfig.APPLICATION_KEY, newApplicationKey);
576 updateConfiguration(configuration);
580 * Register the discovery service.
582 * @param discoveryService new discoveryService.
584 public void registerDiscoveryService(Clip2ThingDiscoveryService discoveryService) {
585 this.discoveryService = discoveryService;
589 * Unregister the discovery service.
591 public void unregisterDiscoveryService() {
592 discoveryService = null;
596 * Update the bridge's online state and update its dependent things. Called when the connection goes online.
598 private void updateOnlineState() {
599 if (assetsLoaded && (thing.getStatus() != ThingStatus.ONLINE)) {
600 logger.debug("updateOnlineState()");
601 connectRetriesRemaining = RECONNECT_MAX_TRIES;
602 updateStatus(ThingStatus.ONLINE);
603 updateThingsScheduled(500);
604 Clip2ThingDiscoveryService discoveryService = this.discoveryService;
605 if (Objects.nonNull(discoveryService)) {
606 discoveryService.startScan(null);
612 * Update the bridge thing properties.
614 * @throws ApiException if a communication error occurred.
615 * @throws AssetNotLoadedException if one of the assets is not loaded.
616 * @throws InterruptedException
618 private void updateProperties() throws ApiException, AssetNotLoadedException, InterruptedException {
619 logger.debug("updateProperties()");
620 Map<String, String> properties = new HashMap<>(thing.getProperties());
622 for (Resource device : getClip2Bridge().getResources(BRIDGE).getResources()) {
623 // set the serial number
624 String bridgeId = device.getBridgeId();
625 if (Objects.nonNull(bridgeId)) {
626 properties.put(Thing.PROPERTY_SERIAL_NUMBER, bridgeId);
631 for (Resource device : getClip2Bridge().getResources(DEVICE).getResources()) {
632 MetaData metaData = device.getMetaData();
633 if (Objects.nonNull(metaData) && metaData.getArchetype() == Archetype.BRIDGE_V2) {
634 // set resource properties
635 properties.put(PROPERTY_RESOURCE_ID, device.getId());
636 properties.put(PROPERTY_RESOURCE_TYPE, device.getType().toString());
638 // set metadata properties
639 String metaDataName = metaData.getName();
640 if (Objects.nonNull(metaDataName)) {
641 properties.put(PROPERTY_RESOURCE_NAME, metaDataName);
643 properties.put(PROPERTY_RESOURCE_ARCHETYPE, metaData.getArchetype().toString());
645 // set product data properties
646 ProductData productData = device.getProductData();
647 if (Objects.nonNull(productData)) {
648 // set generic thing properties
649 properties.put(Thing.PROPERTY_MODEL_ID, productData.getModelId());
650 properties.put(Thing.PROPERTY_VENDOR, productData.getManufacturerName());
651 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, productData.getSoftwareVersion());
652 String hardwarePlatformType = productData.getHardwarePlatformType();
653 if (Objects.nonNull(hardwarePlatformType)) {
654 properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwarePlatformType);
657 // set hue specific properties
658 properties.put(PROPERTY_PRODUCT_NAME, productData.getProductName());
659 properties.put(PROPERTY_PRODUCT_ARCHETYPE, productData.getProductArchetype().toString());
660 properties.put(PROPERTY_PRODUCT_CERTIFIED, productData.getCertified().toString());
662 break; // we only needed the BRIDGE_V2 resource
665 thing.setProperties(properties);
669 * Update the thing's own state. Called sporadically in case any SSE events may have been lost.
671 private void updateSelf() {
672 logger.debug("updateSelf()");
676 getClip2Bridge().open();
677 } catch (ApiException e) {
678 logger.trace("updateSelf() {}", e.getMessage(), e);
679 setStatusOfflineWithCommunicationError(e);
680 onConnectionOffline();
681 } catch (AssetNotLoadedException e) {
682 logger.trace("updateSelf() {}", e.getMessage(), e);
683 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
684 "@text/offline.api2.conf-error.assets-not-loaded");
685 } catch (InterruptedException e) {
690 * Check if a PROPERTY_LEGACY_THING_UID value was set by the discovery process, and if so, clone the legacy thing's
691 * settings into this thing.
693 private void updateThingFromLegacy() {
694 if (isInitialized()) {
695 logger.warn("Cannot update bridge thing '{}' from legacy since handler already initialized.",
699 Map<String, String> properties = thing.getProperties();
700 String legacyThingUID = properties.get(PROPERTY_LEGACY_THING_UID);
701 if (Objects.nonNull(legacyThingUID)) {
702 Thing legacyThing = thingRegistry.get(new ThingUID(legacyThingUID));
703 if (Objects.nonNull(legacyThing)) {
704 BridgeBuilder editBuilder = editThing();
706 String location = legacyThing.getLocation();
707 if (Objects.nonNull(location) && !location.isBlank()) {
708 editBuilder = editBuilder.withLocation(location);
711 Object userName = legacyThing.getConfiguration().get(USER_NAME);
712 if (userName instanceof String) {
713 Configuration configuration = thing.getConfiguration();
714 configuration.put(Clip2BridgeConfig.APPLICATION_KEY, userName);
715 editBuilder = editBuilder.withConfiguration(configuration);
718 Map<String, String> newProperties = new HashMap<>(properties);
719 newProperties.remove(PROPERTY_LEGACY_THING_UID);
721 updateThing(editBuilder.withProperties(newProperties).build());
727 * Execute the mass download of all relevant resource types, and inform all child thing handlers.
729 private void updateThingsNow() {
730 logger.debug("updateThingsNow()");
732 Clip2Bridge bridge = getClip2Bridge();
733 for (ResourceReference reference : MASS_DOWNLOAD_RESOURCE_REFERENCES) {
734 ResourceType resourceType = reference.getType();
735 List<Resource> resourceList = bridge.getResources(reference).getResources();
736 switch (resourceType) {
738 // add special 'All Lights' zone to the zone resource list
739 resourceList.addAll(bridge.getResources(BRIDGE_HOME).getResources());
743 // add 'smart scenes' to the scene resource list
744 resourceList.addAll(bridge.getResources(SMART_SCENE).getResources());
750 getThing().getThings().forEach(thing -> {
751 ThingHandler handler = thing.getHandler();
752 if (handler instanceof Clip2ThingHandler) {
753 ((Clip2ThingHandler) handler).onResourcesList(resourceType, resourceList);
757 } catch (ApiException | AssetNotLoadedException e) {
758 if (logger.isDebugEnabled()) {
759 logger.debug("updateThingsNow() unexpected exception", e);
761 logger.warn("Unexpected exception '{}' while updating things.", e.getMessage());
763 } catch (InterruptedException e) {
768 * Schedule a task to call updateThings(). It prevents floods of GET calls when multiple child things are added at
771 * @param delayMilliSeconds the delay before running the next task.
773 private void updateThingsScheduled(int delayMilliSeconds) {
774 ScheduledFuture<?> task = this.scheduledUpdateTask;
775 if (Objects.isNull(task) || task.getDelay(TimeUnit.MILLISECONDS) < 100) {
776 cancelTask(scheduledUpdateTask, false);
777 scheduledUpdateTask = scheduler.schedule(() -> updateThingsNow(), delayMilliSeconds, TimeUnit.MILLISECONDS);