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.bticinosmarther.internal.handler;
15 import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
17 import java.io.IOException;
18 import java.net.InetAddress;
20 import java.net.URISyntaxException;
21 import java.net.UnknownHostException;
22 import java.time.Duration;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.Optional;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.TimeUnit;
31 import java.util.stream.Collectors;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler;
37 import org.openhab.binding.bticinosmarther.internal.account.SmartherNotificationHandler;
38 import org.openhab.binding.bticinosmarther.internal.api.SmartherApi;
39 import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
40 import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
41 import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
42 import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
43 import org.openhab.binding.bticinosmarther.internal.api.dto.Plant;
44 import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
45 import org.openhab.binding.bticinosmarther.internal.api.dto.Sender;
46 import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription;
47 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException;
48 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
49 import org.openhab.binding.bticinosmarther.internal.config.SmartherBridgeConfiguration;
50 import org.openhab.binding.bticinosmarther.internal.discovery.SmartherModuleDiscoveryService;
51 import org.openhab.binding.bticinosmarther.internal.model.BridgeStatus;
52 import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
53 import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
54 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
55 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
56 import org.openhab.core.auth.client.oauth2.OAuthClientService;
57 import org.openhab.core.auth.client.oauth2.OAuthException;
58 import org.openhab.core.auth.client.oauth2.OAuthFactory;
59 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
60 import org.openhab.core.cache.ExpiringCache;
61 import org.openhab.core.config.core.Configuration;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.thing.Bridge;
65 import org.openhab.core.thing.Channel;
66 import org.openhab.core.thing.ChannelUID;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.ThingUID;
70 import org.openhab.core.thing.binding.BaseBridgeHandler;
71 import org.openhab.core.thing.binding.ThingHandlerService;
72 import org.openhab.core.types.Command;
73 import org.openhab.core.types.RefreshType;
74 import org.openhab.core.types.State;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
79 * The {@code SmartherBridgeHandler} class is responsible of the handling of a Smarther Bridge thing.
80 * The Smarther Bridge is used to manage a set of Smarther Chronothermostat Modules registered under the same
81 * Legrand/Bticino account credentials.
83 * @author Fabio Possieri - Initial contribution
86 public class SmartherBridgeHandler extends BaseBridgeHandler
87 implements SmartherAccountHandler, SmartherNotificationHandler, AccessTokenRefreshListener {
89 private static final long POLL_INITIAL_DELAY = 5;
91 private final Logger logger = LoggerFactory.getLogger(SmartherBridgeHandler.class);
93 private final OAuthFactory oAuthFactory;
94 private final HttpClient httpClient;
96 // Bridge configuration
97 private SmartherBridgeConfiguration config;
99 // Field members assigned in initialize method
100 private @Nullable Future<?> pollFuture;
101 private @Nullable OAuthClientService oAuthService;
102 private @Nullable SmartherApi smartherApi;
103 private @Nullable ExpiringCache<List<Location>> locationCache;
104 private @Nullable BridgeStatus bridgeStatus;
107 * Constructs a {@code SmartherBridgeHandler} for the given Bridge thing, authorization factory and http client.
110 * the {@link Bridge} thing to be used
111 * @param oAuthFactory
112 * the OAuth2 authorization factory to be used
114 * the http client to be used
116 public SmartherBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
118 this.oAuthFactory = oAuthFactory;
119 this.httpClient = httpClient;
120 this.config = new SmartherBridgeConfiguration();
124 public Collection<Class<? extends ThingHandlerService>> getServices() {
125 return Set.of(SmartherModuleDiscoveryService.class);
128 // ===========================================================================
130 // Bridge thing lifecycle management methods
132 // ===========================================================================
135 public void initialize() {
136 logger.debug("Bridge[{}] Initialize handler", thing.getUID());
138 this.config = getConfigAs(SmartherBridgeConfiguration.class);
139 if (StringUtil.isBlank(config.getSubscriptionKey())) {
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141 "The 'Subscription Key' property is not set or empty. If you have an older thing please recreate it.");
144 if (StringUtil.isBlank(config.getClientId())) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
146 "The 'Client Id' property is not set or empty. If you have an older thing please recreate it.");
149 if (StringUtil.isBlank(config.getClientSecret())) {
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
151 "The 'Client Secret' property is not set or empty. If you have an older thing please recreate it.");
155 // Initialize OAuth2 authentication support
156 final OAuthClientService localOAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
157 SMARTHER_API_TOKEN_URL, SMARTHER_AUTHORIZE_URL, config.getClientId(), config.getClientSecret(),
158 SMARTHER_API_SCOPES, false);
159 localOAuthService.addAccessTokenRefreshListener(SmartherBridgeHandler.this);
160 this.oAuthService = localOAuthService;
162 // Initialize Smarther Api
163 final SmartherApi localSmartherApi = new SmartherApi(localOAuthService, config.getSubscriptionKey(), scheduler,
165 this.smartherApi = localSmartherApi;
167 // Initialize locations (plant Ids) local cache
168 final ExpiringCache<List<Location>> localLocationCache = new ExpiringCache<>(
169 Duration.ofMinutes(config.getStatusRefreshPeriod()), this::locationCacheAction);
170 this.locationCache = localLocationCache;
172 // Initialize bridge local status
173 final BridgeStatus localBridgeStatus = new BridgeStatus();
174 this.bridgeStatus = localBridgeStatus;
176 updateStatus(ThingStatus.UNKNOWN);
180 logger.debug("Bridge[{}] Finished initializing!", thing.getUID());
184 public void handleCommand(ChannelUID channelUID, Command command) {
185 switch (channelUID.getId()) {
186 case CHANNEL_CONFIG_FETCH_LOCATIONS:
187 if (command instanceof OnOffType) {
188 if (OnOffType.ON.equals(command)) {
190 "Bridge[{}] Manually triggered channel to remotely fetch the updated client locations list",
194 updateChannelState(CHANNEL_CONFIG_FETCH_LOCATIONS, OnOffType.OFF);
201 if (command instanceof RefreshType) {
202 // Avoid logging wrong command when refresh command is sent
206 logger.debug("Bridge[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command,
207 command.getClass().getTypeName(), channelUID.getId());
211 public void handleRemoval() {
212 super.handleRemoval();
217 public void dispose() {
218 logger.debug("Bridge[{}] Dispose handler", thing.getUID());
219 final OAuthClientService localOAuthService = this.oAuthService;
220 if (localOAuthService != null) {
221 localOAuthService.removeAccessTokenRefreshListener(this);
223 this.oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
225 logger.debug("Bridge[{}] Finished disposing!", thing.getUID());
228 // ===========================================================================
230 // Bridge data cache management methods
232 // ===========================================================================
235 * Returns the available locations to be cached for this Bridge.
237 * @return the available locations to be cached for this Bridge, or {@code null} if the list of available locations
238 * cannot be retrieved
240 private @Nullable List<Location> locationCacheAction() {
242 // Retrieve the plants list from the API Gateway
243 final List<Plant> plants = getPlants();
245 List<Location> locations;
246 if (config.isUseNotifications()) {
247 // Retrieve the subscriptions list from the API Gateway
248 final List<Subscription> subscriptions = getSubscriptions();
250 // Enrich the notifications list with externally registered subscriptions
251 updateNotifications(subscriptions);
253 // Get the notifications list from bridge config
254 final List<String> notifications = config.getNotifications();
256 locations = plants.stream().map(p -> Location.fromPlant(p, subscriptions.stream()
257 .filter(s -> s.getPlantId().equals(p.getId()) && notifications.contains(s.getSubscriptionId()))
258 .findFirst())).collect(Collectors.toList());
260 locations = plants.stream().map(p -> Location.fromPlant(p)).collect(Collectors.toList());
262 logger.debug("Bridge[{}] Available locations: {}", thing.getUID(), locations);
266 } catch (SmartherGatewayException e) {
267 logger.warn("Bridge[{}] Cannot retrieve available locations: {}", thing.getUID(), e.getMessage());
273 * Updates this Bridge local notifications list with externally registered subscriptions.
275 * @param subscriptions
276 * the externally registered subscriptions to be added to the local notifications list
278 private void updateNotifications(List<Subscription> subscriptions) {
279 // Get the notifications list from bridge config
280 List<String> notifications = config.getNotifications();
282 for (Subscription s : subscriptions) {
283 if (s.getEndpointUrl().equalsIgnoreCase(config.getNotificationUrl())
284 && !notifications.contains(s.getSubscriptionId())) {
285 // Add the external subscription to notifications list
286 notifications = config.addNotification(s.getSubscriptionId());
288 // Save the updated notifications list back to bridge config
289 Configuration configuration = editConfiguration();
290 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
291 updateConfiguration(configuration);
297 * Sets all the cache to "expired" for this Bridge.
299 private void expireCache() {
300 logger.debug("Bridge[{}] Invalidating location cache", thing.getUID());
301 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
302 if (localLocationCache != null) {
303 localLocationCache.invalidateValue();
307 // ===========================================================================
309 // Bridge status polling mechanism methods
311 // ===========================================================================
314 * Starts a new scheduler to periodically poll and update this Bridge status.
316 private void schedulePoll() {
319 // Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period
320 final Future<?> localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY,
321 config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS);
322 this.pollFuture = localPollFuture;
324 logger.debug("Bridge[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY,
325 config.getStatusRefreshPeriod());
329 * Cancels all running poll schedulers.
331 * @param mayInterruptIfRunning
332 * {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
333 * tasks are allowed to complete
335 private synchronized void stopPoll(boolean mayInterruptIfRunning) {
336 final Future<?> localPollFuture = this.pollFuture;
337 if (localPollFuture != null) {
338 if (!localPollFuture.isCancelled()) {
339 localPollFuture.cancel(mayInterruptIfRunning);
341 this.pollFuture = null;
346 * Polls to update this Bridge status, calling the Smarther API to refresh its plants list.
348 * @return {@code true} if the method completes without errors, {@code false} otherwise
350 private synchronized boolean poll() {
352 onAccessTokenResponse(getAccessTokenResponse());
357 updateStatus(ThingStatus.ONLINE);
359 } catch (SmartherAuthorizationException e) {
360 logger.warn("Bridge[{}] Authorization error during polling: {}", thing.getUID(), e.getMessage());
361 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
362 } catch (RuntimeException e) {
363 // All other exceptions apart from Authorization and Gateway issues
364 logger.warn("Bridge[{}] Unexpected error during polling, please report if this keeps occurring: ",
366 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
373 public void onAccessTokenResponse(@Nullable AccessTokenResponse tokenResponse) {
374 logger.trace("Bridge[{}] Got access token: {}", thing.getUID(),
375 (tokenResponse != null) ? tokenResponse.getAccessToken() : "none");
378 // ===========================================================================
380 // Bridge convenience methods
382 // ===========================================================================
385 * Convenience method to get this Bridge configuration.
387 * @return a {@link SmartherBridgeConfiguration} object containing the Bridge configuration
389 public SmartherBridgeConfiguration getSmartherBridgeConfig() {
394 * Convenience method to get the access token from Smarther API authorization layer.
396 * @return the autorization access token, may be {@code null}
398 * @throws {@link SmartherAuthorizationException}
399 * in case of authorization issues with the Smarther API
401 private @Nullable AccessTokenResponse getAccessTokenResponse() throws SmartherAuthorizationException {
403 final OAuthClientService localOAuthService = this.oAuthService;
404 if (localOAuthService != null) {
405 return localOAuthService.getAccessTokenResponse();
408 } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
409 throw new SmartherAuthorizationException(e.getMessage());
414 * Convenience method to update the given Channel state "only" if the Channel is linked.
417 * the identifier of the Channel to be updated
419 * the new state to be applied to the given Channel
421 private void updateChannelState(String channelId, State state) {
422 final Channel channel = thing.getChannel(channelId);
424 if (channel != null && isLinked(channel.getUID())) {
425 updateState(channel.getUID(), state);
430 * Convenience method to update the Smarther API calls counter for this Bridge.
432 private void updateApiCallsCounter() {
433 final BridgeStatus localBridgeStatus = this.bridgeStatus;
434 if (localBridgeStatus != null) {
435 updateChannelState(CHANNEL_STATUS_API_CALLS_HANDLED,
436 new DecimalType(localBridgeStatus.incrementApiCallsHandled()));
441 * Convenience method to check and get the Smarther API instance for this Bridge.
443 * @return the Smarther API instance
445 * @throws {@link SmartherGatewayException}
446 * in case the Smarther API instance is {@code null}
448 private SmartherApi getSmartherApi() throws SmartherGatewayException {
449 final SmartherApi localSmartherApi = this.smartherApi;
450 if (localSmartherApi == null) {
451 throw new SmartherGatewayException("Smarther API instance is null");
453 return localSmartherApi;
456 // ===========================================================================
458 // Implementation of the SmartherAccountHandler interface
460 // ===========================================================================
463 public ThingUID getUID() {
464 return thing.getUID();
468 public String getLabel() {
469 return StringUtil.defaultString(thing.getLabel());
473 public List<Location> getLocations() {
474 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
475 final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
476 return (locations != null) ? locations : Collections.emptyList();
480 public boolean hasLocation(String plantId) {
481 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
482 final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
483 return (locations != null) ? locations.stream().anyMatch(l -> l.getPlantId().equals(plantId)) : false;
487 public List<Plant> getPlants() throws SmartherGatewayException {
488 updateApiCallsCounter();
489 return getSmartherApi().getPlants();
493 public List<Subscription> getSubscriptions() throws SmartherGatewayException {
494 updateApiCallsCounter();
495 return getSmartherApi().getSubscriptions();
499 public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException {
500 updateApiCallsCounter();
501 return getSmartherApi().subscribePlant(plantId, notificationUrl);
505 public void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException {
506 updateApiCallsCounter();
507 getSmartherApi().unsubscribePlant(plantId, subscriptionId);
511 public List<Module> getLocationModules(Location location) {
513 updateApiCallsCounter();
514 return getSmartherApi().getPlantModules(location.getPlantId());
515 } catch (SmartherGatewayException e) {
516 return new ArrayList<>();
521 public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException {
522 updateApiCallsCounter();
523 return getSmartherApi().getModuleStatus(plantId, moduleId);
527 public boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException {
528 updateApiCallsCounter();
529 return getSmartherApi().setModuleStatus(moduleSettings);
533 public List<Program> getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException {
534 updateApiCallsCounter();
535 return getSmartherApi().getModulePrograms(plantId, moduleId);
539 public boolean isAuthorized() {
541 final AccessTokenResponse tokenResponse = getAccessTokenResponse();
542 onAccessTokenResponse(tokenResponse);
544 return (tokenResponse != null && tokenResponse.getAccessToken() != null
545 && tokenResponse.getRefreshToken() != null);
546 } catch (SmartherAuthorizationException e) {
552 public boolean isOnline() {
553 return (thing.getStatus() == ThingStatus.ONLINE);
557 public String authorize(String redirectUrl, String reqCode, String notificationUrl)
558 throws SmartherGatewayException {
560 logger.debug("Bridge[{}] Call API gateway to get access token. RedirectUri: {}", thing.getUID(),
563 final OAuthClientService localOAuthService = this.oAuthService;
564 if (localOAuthService == null) {
565 throw new SmartherAuthorizationException("Authorization service is null");
568 // OAuth2 call to get access token from received authorization code
569 localOAuthService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUrl);
571 // Store the notification URL in bridge configuration
572 Configuration configuration = editConfiguration();
573 configuration.put(PROPERTY_NOTIFICATION_URL, notificationUrl);
574 updateConfiguration(configuration);
575 config.setNotificationUrl(notificationUrl);
576 logger.debug("Bridge[{}] Store notification URL: {}", thing.getUID(), notificationUrl);
578 // Reschedule the polling thread
581 return config.getClientId();
582 } catch (OAuthResponseException e) {
583 throw new SmartherAuthorizationException(e.toString(), e);
584 } catch (OAuthException | IOException e) {
585 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
586 throw new SmartherGatewayException(e.getMessage(), e);
591 public boolean equalsThingUID(String thingUID) {
592 return thing.getUID().getAsString().equals(thingUID);
596 public String formatAuthorizationUrl(String redirectUri) {
598 final OAuthClientService localOAuthService = this.oAuthService;
599 if (localOAuthService != null) {
600 return localOAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
602 } catch (OAuthException e) {
603 logger.warn("Bridge[{}] Error constructing AuthorizationUrl: {}", thing.getUID(), e.getMessage());
608 // ===========================================================================
610 // Implementation of the SmartherNotificationHandler interface
612 // ===========================================================================
615 public boolean useNotifications() {
616 return config.isUseNotifications();
620 public synchronized void registerNotification(String plantId) throws SmartherGatewayException {
621 if (!config.isUseNotifications()) {
625 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
626 if (localLocationCache != null) {
627 List<Location> locations = localLocationCache.getValue();
628 if (locations != null) {
629 final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
631 if (maybeLocation.isPresent()) {
632 Location location = maybeLocation.get();
633 if (!location.hasSubscription()) {
634 // Validate notification Url (must be non-null and https)
635 final String notificationUrl = config.getNotificationUrl();
636 if (isValidNotificationUrl(notificationUrl)) {
637 // Call gateway to register plant subscription
638 String subscriptionId = subscribePlant(plantId, config.getNotificationUrl());
639 logger.debug("Bridge[{}] Notification registered: [plantId={}, subscriptionId={}]",
640 thing.getUID(), plantId, subscriptionId);
642 // Add the new subscription to notifications list
643 List<String> notifications = config.addNotification(subscriptionId);
645 // Save the updated notifications list back to bridge config
646 Configuration configuration = editConfiguration();
647 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
648 updateConfiguration(configuration);
650 // Update the local locationCache with the added data
651 locations.stream().forEach(l -> {
652 if (l.getPlantId().equals(plantId)) {
653 l.setSubscription(subscriptionId, config.getNotificationUrl());
656 localLocationCache.putValue(locations);
659 "Bridge[{}] Invalid notification Url [{}]: must be non-null, public https address",
660 thing.getUID(), notificationUrl);
669 public void handleNotification(Notification notification) {
670 final Sender sender = notification.getSender();
671 if (sender != null) {
672 final BridgeStatus localBridgeStatus = this.bridgeStatus;
673 if (localBridgeStatus != null) {
674 logger.debug("Bridge[{}] Notification received: [id={}]", thing.getUID(), notification.getId());
675 updateChannelState(CHANNEL_STATUS_NOTIFS_RECEIVED,
676 new DecimalType(localBridgeStatus.incrementNotificationsReceived()));
678 final String plantId = sender.getPlant().getId();
679 final String moduleId = sender.getPlant().getModule().getId();
680 Optional<SmartherModuleHandler> maybeModuleHandler = getThing().getThings().stream()
681 .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.isLinkedTo(plantId, moduleId))
684 if (config.isUseNotifications() && maybeModuleHandler.isPresent()) {
685 maybeModuleHandler.get().handleNotification(notification);
687 logger.debug("Bridge[{}] Notification rejected: no module handler available", thing.getUID());
688 updateChannelState(CHANNEL_STATUS_NOTIFS_REJECTED,
689 new DecimalType(localBridgeStatus.incrementNotificationsRejected()));
696 public synchronized void unregisterNotification(String plantId) throws SmartherGatewayException {
697 if (!config.isUseNotifications()) {
701 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
702 if (localLocationCache != null) {
703 List<Location> locations = localLocationCache.getValue();
705 final long remainingModules = getThing().getThings().stream()
706 .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.getPlantId().equals(plantId))
709 if (locations != null && remainingModules == 0) {
710 final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
712 if (maybeLocation.isPresent()) {
713 Location location = maybeLocation.get();
714 final String subscriptionId = location.getSubscriptionId();
715 if (location.hasSubscription() && (subscriptionId != null)) {
716 // Call gateway to unregister plant subscription
717 unsubscribePlant(plantId, subscriptionId);
718 logger.debug("Bridge[{}] Notification unregistered: [plantId={}, subscriptionId={}]",
719 thing.getUID(), plantId, subscriptionId);
721 // Remove the subscription from notifications list
722 List<String> notifications = config.removeNotification(subscriptionId);
724 // Save the updated notifications list back to bridge config
725 Configuration configuration = editConfiguration();
726 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
727 updateConfiguration(configuration);
729 // Update the local locationCache with the removed data
730 locations.stream().forEach(l -> {
731 if (l.getPlantId().equals(plantId)) {
732 l.unsetSubscription();
735 localLocationCache.putValue(locations);
743 * Checks if the passed string is a formally valid Notification Url (non-null, public https address).
746 * the string to check
748 * @return {@code true} if the given string is a formally valid Notification Url, {@code false} otherwise
750 private boolean isValidNotificationUrl(@Nullable String str) {
753 URI maybeValidNotificationUrl = new URI(str);
754 if (HTTPS_SCHEMA.equals(maybeValidNotificationUrl.getScheme())) {
755 InetAddress address = InetAddress.getByName(maybeValidNotificationUrl.getHost());
756 if (!address.isLoopbackAddress() && !address.isSiteLocalAddress()) {
762 } catch (URISyntaxException | UnknownHostException e) {