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;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.bticinosmarther.internal.account.SmartherAccountHandler;
36 import org.openhab.binding.bticinosmarther.internal.account.SmartherNotificationHandler;
37 import org.openhab.binding.bticinosmarther.internal.api.SmartherApi;
38 import org.openhab.binding.bticinosmarther.internal.api.dto.Location;
39 import org.openhab.binding.bticinosmarther.internal.api.dto.Module;
40 import org.openhab.binding.bticinosmarther.internal.api.dto.ModuleStatus;
41 import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
42 import org.openhab.binding.bticinosmarther.internal.api.dto.Plant;
43 import org.openhab.binding.bticinosmarther.internal.api.dto.Program;
44 import org.openhab.binding.bticinosmarther.internal.api.dto.Sender;
45 import org.openhab.binding.bticinosmarther.internal.api.dto.Subscription;
46 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherAuthorizationException;
47 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
48 import org.openhab.binding.bticinosmarther.internal.config.SmartherBridgeConfiguration;
49 import org.openhab.binding.bticinosmarther.internal.discovery.SmartherModuleDiscoveryService;
50 import org.openhab.binding.bticinosmarther.internal.model.BridgeStatus;
51 import org.openhab.binding.bticinosmarther.internal.model.ModuleSettings;
52 import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
53 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
54 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
55 import org.openhab.core.auth.client.oauth2.OAuthClientService;
56 import org.openhab.core.auth.client.oauth2.OAuthException;
57 import org.openhab.core.auth.client.oauth2.OAuthFactory;
58 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
59 import org.openhab.core.cache.ExpiringCache;
60 import org.openhab.core.config.core.Configuration;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.thing.Bridge;
64 import org.openhab.core.thing.Channel;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.ThingStatus;
67 import org.openhab.core.thing.ThingStatusDetail;
68 import org.openhab.core.thing.ThingUID;
69 import org.openhab.core.thing.binding.BaseBridgeHandler;
70 import org.openhab.core.thing.binding.ThingHandlerService;
71 import org.openhab.core.types.Command;
72 import org.openhab.core.types.RefreshType;
73 import org.openhab.core.types.State;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
78 * The {@code SmartherBridgeHandler} class is responsible of the handling of a Smarther Bridge thing.
79 * The Smarther Bridge is used to manage a set of Smarther Chronothermostat Modules registered under the same
80 * Legrand/Bticino account credentials.
82 * @author Fabio Possieri - Initial contribution
85 public class SmartherBridgeHandler extends BaseBridgeHandler
86 implements SmartherAccountHandler, SmartherNotificationHandler, AccessTokenRefreshListener {
88 private static final long POLL_INITIAL_DELAY = 5;
90 private final Logger logger = LoggerFactory.getLogger(SmartherBridgeHandler.class);
92 private final OAuthFactory oAuthFactory;
93 private final HttpClient httpClient;
95 // Bridge configuration
96 private SmartherBridgeConfiguration config;
98 // Field members assigned in initialize method
99 private @Nullable Future<?> pollFuture;
100 private @Nullable OAuthClientService oAuthService;
101 private @Nullable SmartherApi smartherApi;
102 private @Nullable ExpiringCache<List<Location>> locationCache;
103 private @Nullable BridgeStatus bridgeStatus;
106 * Constructs a {@code SmartherBridgeHandler} for the given Bridge thing, authorization factory and http client.
109 * the {@link Bridge} thing to be used
110 * @param oAuthFactory
111 * the OAuth2 authorization factory to be used
113 * the http client to be used
115 public SmartherBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
117 this.oAuthFactory = oAuthFactory;
118 this.httpClient = httpClient;
119 this.config = new SmartherBridgeConfiguration();
123 public Collection<Class<? extends ThingHandlerService>> getServices() {
124 return Collections.singleton(SmartherModuleDiscoveryService.class);
127 // ===========================================================================
129 // Bridge thing lifecycle management methods
131 // ===========================================================================
134 public void initialize() {
135 logger.debug("Bridge[{}] Initialize handler", thing.getUID());
137 this.config = getConfigAs(SmartherBridgeConfiguration.class);
138 if (StringUtil.isBlank(config.getSubscriptionKey())) {
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
140 "The 'Subscription Key' property is not set or empty. If you have an older thing please recreate it.");
143 if (StringUtil.isBlank(config.getClientId())) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "The 'Client Id' property is not set or empty. If you have an older thing please recreate it.");
148 if (StringUtil.isBlank(config.getClientSecret())) {
149 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
150 "The 'Client Secret' property is not set or empty. If you have an older thing please recreate it.");
154 // Initialize OAuth2 authentication support
155 final OAuthClientService localOAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(),
156 SMARTHER_API_TOKEN_URL, SMARTHER_AUTHORIZE_URL, config.getClientId(), config.getClientSecret(),
157 SMARTHER_API_SCOPES, false);
158 localOAuthService.addAccessTokenRefreshListener(SmartherBridgeHandler.this);
159 this.oAuthService = localOAuthService;
161 // Initialize Smarther Api
162 final SmartherApi localSmartherApi = new SmartherApi(localOAuthService, config.getSubscriptionKey(), scheduler,
164 this.smartherApi = localSmartherApi;
166 // Initialize locations (plant Ids) local cache
167 final ExpiringCache<List<Location>> localLocationCache = new ExpiringCache<>(
168 Duration.ofMinutes(config.getStatusRefreshPeriod()), this::locationCacheAction);
169 this.locationCache = localLocationCache;
171 // Initialize bridge local status
172 final BridgeStatus localBridgeStatus = new BridgeStatus();
173 this.bridgeStatus = localBridgeStatus;
175 updateStatus(ThingStatus.UNKNOWN);
179 logger.debug("Bridge[{}] Finished initializing!", thing.getUID());
183 public void handleCommand(ChannelUID channelUID, Command command) {
184 switch (channelUID.getId()) {
185 case CHANNEL_CONFIG_FETCH_LOCATIONS:
186 if (command instanceof OnOffType) {
187 if (OnOffType.ON.equals(command)) {
189 "Bridge[{}] Manually triggered channel to remotely fetch the updated client locations list",
193 updateChannelState(CHANNEL_CONFIG_FETCH_LOCATIONS, OnOffType.OFF);
200 if (command instanceof RefreshType) {
201 // Avoid logging wrong command when refresh command is sent
205 logger.debug("Bridge[{}] Received command {} of wrong type {} on channel {}", thing.getUID(), command,
206 command.getClass().getTypeName(), channelUID.getId());
210 public void handleRemoval() {
211 super.handleRemoval();
216 public void dispose() {
217 logger.debug("Bridge[{}] Dispose handler", thing.getUID());
218 final OAuthClientService localOAuthService = this.oAuthService;
219 if (localOAuthService != null) {
220 localOAuthService.removeAccessTokenRefreshListener(this);
222 this.oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
224 logger.debug("Bridge[{}] Finished disposing!", thing.getUID());
227 // ===========================================================================
229 // Bridge data cache management methods
231 // ===========================================================================
234 * Returns the available locations to be cached for this Bridge.
236 * @return the available locations to be cached for this Bridge, or {@code null} if the list of available locations
237 * cannot be retrieved
239 private @Nullable List<Location> locationCacheAction() {
241 // Retrieve the plants list from the API Gateway
242 final List<Plant> plants = getPlants();
244 List<Location> locations;
245 if (config.isUseNotifications()) {
246 // Retrieve the subscriptions list from the API Gateway
247 final List<Subscription> subscriptions = getSubscriptions();
249 // Enrich the notifications list with externally registered subscriptions
250 updateNotifications(subscriptions);
252 // Get the notifications list from bridge config
253 final List<String> notifications = config.getNotifications();
255 locations = plants.stream().map(p -> Location.fromPlant(p, subscriptions.stream()
256 .filter(s -> s.getPlantId().equals(p.getId()) && notifications.contains(s.getSubscriptionId()))
257 .findFirst())).collect(Collectors.toList());
259 locations = plants.stream().map(p -> Location.fromPlant(p)).collect(Collectors.toList());
261 logger.debug("Bridge[{}] Available locations: {}", thing.getUID(), locations);
265 } catch (SmartherGatewayException e) {
266 logger.warn("Bridge[{}] Cannot retrieve available locations: {}", thing.getUID(), e.getMessage());
272 * Updates this Bridge local notifications list with externally registered subscriptions.
274 * @param subscriptions
275 * the externally registered subscriptions to be added to the local notifications list
277 private void updateNotifications(List<Subscription> subscriptions) {
278 // Get the notifications list from bridge config
279 List<String> notifications = config.getNotifications();
281 for (Subscription s : subscriptions) {
282 if (s.getEndpointUrl().equalsIgnoreCase(config.getNotificationUrl())
283 && !notifications.contains(s.getSubscriptionId())) {
284 // Add the external subscription to notifications list
285 notifications = config.addNotification(s.getSubscriptionId());
287 // Save the updated notifications list back to bridge config
288 Configuration configuration = editConfiguration();
289 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
290 updateConfiguration(configuration);
296 * Sets all the cache to "expired" for this Bridge.
298 private void expireCache() {
299 logger.debug("Bridge[{}] Invalidating location cache", thing.getUID());
300 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
301 if (localLocationCache != null) {
302 localLocationCache.invalidateValue();
306 // ===========================================================================
308 // Bridge status polling mechanism methods
310 // ===========================================================================
313 * Starts a new scheduler to periodically poll and update this Bridge status.
315 private void schedulePoll() {
318 // Schedule poll to start after POLL_INITIAL_DELAY sec and run periodically based on status refresh period
319 final Future<?> localPollFuture = scheduler.scheduleWithFixedDelay(this::poll, POLL_INITIAL_DELAY,
320 config.getStatusRefreshPeriod() * 60, TimeUnit.SECONDS);
321 this.pollFuture = localPollFuture;
323 logger.debug("Bridge[{}] Scheduled poll for {} sec out, then every {} min", thing.getUID(), POLL_INITIAL_DELAY,
324 config.getStatusRefreshPeriod());
328 * Cancels all running poll schedulers.
330 * @param mayInterruptIfRunning
331 * {@code true} if the thread executing this task should be interrupted, {@code false} if the in-progress
332 * tasks are allowed to complete
334 private synchronized void stopPoll(boolean mayInterruptIfRunning) {
335 final Future<?> localPollFuture = this.pollFuture;
336 if (localPollFuture != null) {
337 if (!localPollFuture.isCancelled()) {
338 localPollFuture.cancel(mayInterruptIfRunning);
340 this.pollFuture = null;
345 * Polls to update this Bridge status, calling the Smarther API to refresh its plants list.
347 * @return {@code true} if the method completes without errors, {@code false} otherwise
349 private synchronized boolean poll() {
351 onAccessTokenResponse(getAccessTokenResponse());
356 updateStatus(ThingStatus.ONLINE);
358 } catch (SmartherAuthorizationException e) {
359 logger.warn("Bridge[{}] Authorization error during polling: {}", thing.getUID(), e.getMessage());
360 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
361 } catch (RuntimeException e) {
362 // All other exceptions apart from Authorization and Gateway issues
363 logger.warn("Bridge[{}] Unexpected error during polling, please report if this keeps occurring: ",
365 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
372 public void onAccessTokenResponse(@Nullable AccessTokenResponse tokenResponse) {
373 logger.trace("Bridge[{}] Got access token: {}", thing.getUID(),
374 (tokenResponse != null) ? tokenResponse.getAccessToken() : "none");
377 // ===========================================================================
379 // Bridge convenience methods
381 // ===========================================================================
384 * Convenience method to get this Bridge configuration.
386 * @return a {@link SmartherBridgeConfiguration} object containing the Bridge configuration
388 public SmartherBridgeConfiguration getSmartherBridgeConfig() {
393 * Convenience method to get the access token from Smarther API authorization layer.
395 * @return the autorization access token, may be {@code null}
397 * @throws {@link SmartherAuthorizationException}
398 * in case of authorization issues with the Smarther API
400 private @Nullable AccessTokenResponse getAccessTokenResponse() throws SmartherAuthorizationException {
402 final OAuthClientService localOAuthService = this.oAuthService;
403 if (localOAuthService != null) {
404 return localOAuthService.getAccessTokenResponse();
407 } catch (OAuthException | IOException | OAuthResponseException | RuntimeException e) {
408 throw new SmartherAuthorizationException(e.getMessage());
413 * Convenience method to update the given Channel state "only" if the Channel is linked.
416 * the identifier of the Channel to be updated
418 * the new state to be applied to the given Channel
420 private void updateChannelState(String channelId, State state) {
421 final Channel channel = thing.getChannel(channelId);
423 if (channel != null && isLinked(channel.getUID())) {
424 updateState(channel.getUID(), state);
429 * Convenience method to update the Smarther API calls counter for this Bridge.
431 private void updateApiCallsCounter() {
432 final BridgeStatus localBridgeStatus = this.bridgeStatus;
433 if (localBridgeStatus != null) {
434 updateChannelState(CHANNEL_STATUS_API_CALLS_HANDLED,
435 new DecimalType(localBridgeStatus.incrementApiCallsHandled()));
440 * Convenience method to check and get the Smarther API instance for this Bridge.
442 * @return the Smarther API instance
444 * @throws {@link SmartherGatewayException}
445 * in case the Smarther API instance is {@code null}
447 private SmartherApi getSmartherApi() throws SmartherGatewayException {
448 final SmartherApi localSmartherApi = this.smartherApi;
449 if (localSmartherApi == null) {
450 throw new SmartherGatewayException("Smarther API instance is null");
452 return localSmartherApi;
455 // ===========================================================================
457 // Implementation of the SmartherAccountHandler interface
459 // ===========================================================================
462 public ThingUID getUID() {
463 return thing.getUID();
467 public String getLabel() {
468 return StringUtil.defaultString(thing.getLabel());
472 public List<Location> getLocations() {
473 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
474 final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
475 return (locations != null) ? locations : Collections.emptyList();
479 public boolean hasLocation(String plantId) {
480 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
481 final List<Location> locations = (localLocationCache != null) ? localLocationCache.getValue() : null;
482 return (locations != null) ? locations.stream().anyMatch(l -> l.getPlantId().equals(plantId)) : false;
486 public List<Plant> getPlants() throws SmartherGatewayException {
487 updateApiCallsCounter();
488 return getSmartherApi().getPlants();
492 public List<Subscription> getSubscriptions() throws SmartherGatewayException {
493 updateApiCallsCounter();
494 return getSmartherApi().getSubscriptions();
498 public String subscribePlant(String plantId, String notificationUrl) throws SmartherGatewayException {
499 updateApiCallsCounter();
500 return getSmartherApi().subscribePlant(plantId, notificationUrl);
504 public void unsubscribePlant(String plantId, String subscriptionId) throws SmartherGatewayException {
505 updateApiCallsCounter();
506 getSmartherApi().unsubscribePlant(plantId, subscriptionId);
510 public List<Module> getLocationModules(Location location) {
512 updateApiCallsCounter();
513 return getSmartherApi().getPlantModules(location.getPlantId());
514 } catch (SmartherGatewayException e) {
515 return new ArrayList<>();
520 public ModuleStatus getModuleStatus(String plantId, String moduleId) throws SmartherGatewayException {
521 updateApiCallsCounter();
522 return getSmartherApi().getModuleStatus(plantId, moduleId);
526 public boolean setModuleStatus(ModuleSettings moduleSettings) throws SmartherGatewayException {
527 updateApiCallsCounter();
528 return getSmartherApi().setModuleStatus(moduleSettings);
532 public List<Program> getModulePrograms(String plantId, String moduleId) throws SmartherGatewayException {
533 updateApiCallsCounter();
534 return getSmartherApi().getModulePrograms(plantId, moduleId);
538 public boolean isAuthorized() {
540 final AccessTokenResponse tokenResponse = getAccessTokenResponse();
541 onAccessTokenResponse(tokenResponse);
543 return (tokenResponse != null && tokenResponse.getAccessToken() != null
544 && tokenResponse.getRefreshToken() != null);
545 } catch (SmartherAuthorizationException e) {
551 public boolean isOnline() {
552 return (thing.getStatus() == ThingStatus.ONLINE);
556 public String authorize(String redirectUrl, String reqCode, String notificationUrl)
557 throws SmartherGatewayException {
559 logger.debug("Bridge[{}] Call API gateway to get access token. RedirectUri: {}", thing.getUID(),
562 final OAuthClientService localOAuthService = this.oAuthService;
563 if (localOAuthService == null) {
564 throw new SmartherAuthorizationException("Authorization service is null");
567 // OAuth2 call to get access token from received authorization code
568 localOAuthService.getAccessTokenResponseByAuthorizationCode(reqCode, redirectUrl);
570 // Store the notification URL in bridge configuration
571 Configuration configuration = editConfiguration();
572 configuration.put(PROPERTY_NOTIFICATION_URL, notificationUrl);
573 updateConfiguration(configuration);
574 config.setNotificationUrl(notificationUrl);
575 logger.debug("Bridge[{}] Store notification URL: {}", thing.getUID(), notificationUrl);
577 // Reschedule the polling thread
580 return config.getClientId();
581 } catch (OAuthResponseException e) {
582 throw new SmartherAuthorizationException(e.toString(), e);
583 } catch (OAuthException | IOException e) {
584 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
585 throw new SmartherGatewayException(e.getMessage(), e);
590 public boolean equalsThingUID(String thingUID) {
591 return thing.getUID().getAsString().equals(thingUID);
595 public String formatAuthorizationUrl(String redirectUri) {
597 final OAuthClientService localOAuthService = this.oAuthService;
598 if (localOAuthService != null) {
599 return localOAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
601 } catch (OAuthException e) {
602 logger.warn("Bridge[{}] Error constructing AuthorizationUrl: {}", thing.getUID(), e.getMessage());
607 // ===========================================================================
609 // Implementation of the SmartherNotificationHandler interface
611 // ===========================================================================
614 public boolean useNotifications() {
615 return config.isUseNotifications();
619 public synchronized void registerNotification(String plantId) throws SmartherGatewayException {
620 if (!config.isUseNotifications()) {
624 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
625 if (localLocationCache != null) {
626 List<Location> locations = localLocationCache.getValue();
627 if (locations != null) {
628 final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
630 if (maybeLocation.isPresent()) {
631 Location location = maybeLocation.get();
632 if (!location.hasSubscription()) {
633 // Validate notification Url (must be non-null and https)
634 final String notificationUrl = config.getNotificationUrl();
635 if (isValidNotificationUrl(notificationUrl)) {
636 // Call gateway to register plant subscription
637 String subscriptionId = subscribePlant(plantId, config.getNotificationUrl());
638 logger.debug("Bridge[{}] Notification registered: [plantId={}, subscriptionId={}]",
639 thing.getUID(), plantId, subscriptionId);
641 // Add the new subscription to notifications list
642 List<String> notifications = config.addNotification(subscriptionId);
644 // Save the updated notifications list back to bridge config
645 Configuration configuration = editConfiguration();
646 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
647 updateConfiguration(configuration);
649 // Update the local locationCache with the added data
650 locations.stream().forEach(l -> {
651 if (l.getPlantId().equals(plantId)) {
652 l.setSubscription(subscriptionId, config.getNotificationUrl());
655 localLocationCache.putValue(locations);
658 "Bridge[{}] Invalid notification Url [{}]: must be non-null, public https address",
659 thing.getUID(), notificationUrl);
668 public void handleNotification(Notification notification) {
669 final Sender sender = notification.getSender();
670 if (sender != null) {
671 final BridgeStatus localBridgeStatus = this.bridgeStatus;
672 if (localBridgeStatus != null) {
673 logger.debug("Bridge[{}] Notification received: [id={}]", thing.getUID(), notification.getId());
674 updateChannelState(CHANNEL_STATUS_NOTIFS_RECEIVED,
675 new DecimalType(localBridgeStatus.incrementNotificationsReceived()));
677 final String plantId = sender.getPlant().getId();
678 final String moduleId = sender.getPlant().getModule().getId();
679 Optional<SmartherModuleHandler> maybeModuleHandler = getThing().getThings().stream()
680 .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.isLinkedTo(plantId, moduleId))
683 if (config.isUseNotifications() && maybeModuleHandler.isPresent()) {
684 maybeModuleHandler.get().handleNotification(notification);
686 logger.debug("Bridge[{}] Notification rejected: no module handler available", thing.getUID());
687 updateChannelState(CHANNEL_STATUS_NOTIFS_REJECTED,
688 new DecimalType(localBridgeStatus.incrementNotificationsRejected()));
695 public synchronized void unregisterNotification(String plantId) throws SmartherGatewayException {
696 if (!config.isUseNotifications()) {
700 final ExpiringCache<List<Location>> localLocationCache = this.locationCache;
701 if (localLocationCache != null) {
702 List<Location> locations = localLocationCache.getValue();
704 final long remainingModules = getThing().getThings().stream()
705 .map(t -> (SmartherModuleHandler) t.getHandler()).filter(h -> h.getPlantId().equals(plantId))
708 if (locations != null && remainingModules == 0) {
709 final Optional<Location> maybeLocation = locations.stream().filter(l -> l.getPlantId().equals(plantId))
711 if (maybeLocation.isPresent()) {
712 Location location = maybeLocation.get();
713 final String subscriptionId = location.getSubscriptionId();
714 if (location.hasSubscription() && (subscriptionId != null)) {
715 // Call gateway to unregister plant subscription
716 unsubscribePlant(plantId, subscriptionId);
717 logger.debug("Bridge[{}] Notification unregistered: [plantId={}, subscriptionId={}]",
718 thing.getUID(), plantId, subscriptionId);
720 // Remove the subscription from notifications list
721 List<String> notifications = config.removeNotification(subscriptionId);
723 // Save the updated notifications list back to bridge config
724 Configuration configuration = editConfiguration();
725 configuration.put(PROPERTY_NOTIFICATIONS, notifications);
726 updateConfiguration(configuration);
728 // Update the local locationCache with the removed data
729 locations.stream().forEach(l -> {
730 if (l.getPlantId().equals(plantId)) {
731 l.unsetSubscription();
734 localLocationCache.putValue(locations);
742 * Checks if the passed string is a formally valid Notification Url (non-null, public https address).
745 * the string to check
747 * @return {@code true} if the given string is a formally valid Notification Url, {@code false} otherwise
749 private boolean isValidNotificationUrl(@Nullable String str) {
752 URI maybeValidNotificationUrl = new URI(str);
753 if (HTTPS_SCHEMA.equals(maybeValidNotificationUrl.getScheme())) {
754 InetAddress address = InetAddress.getByName(maybeValidNotificationUrl.getHost());
755 if (!address.isLoopbackAddress() && !address.isSiteLocalAddress()) {
761 } catch (URISyntaxException | UnknownHostException e) {