]> git.basschouten.com Git - openhab-addons.git/blob
3db6e5b4d0d2a3532adbb85a34c87766313c59a2
[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.nuki.internal.handler;
14
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.function.Consumer;
23 import java.util.function.Function;
24
25 import javax.ws.rs.core.UriBuilder;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.http.HttpStatus;
31 import org.openhab.binding.nuki.internal.configuration.NukiBridgeConfiguration;
32 import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
33 import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
34 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackAddResponse;
35 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackListResponse;
36 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackRemoveResponse;
37 import org.openhab.binding.nuki.internal.dataexchange.BridgeInfoResponse;
38 import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
39 import org.openhab.binding.nuki.internal.discovery.NukiDeviceDiscoveryService;
40 import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListCallbackDto;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link NukiBridgeHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Markus Katter - Initial contribution
56  * @contributer Jan Vybíral - Improved callback handling
57  */
58 @NonNullByDefault
59 public class NukiBridgeHandler extends BaseBridgeHandler {
60
61     private final Logger logger = LoggerFactory.getLogger(NukiBridgeHandler.class);
62     private static final int JOB_INTERVAL = 600;
63
64     private final HttpClient httpClient;
65     @Nullable
66     private NukiHttpClient nukiHttpClient;
67     @Nullable
68     private final String callbackUrl;
69     @Nullable
70     private ScheduledFuture<?> checkBridgeOnlineJob;
71     private NukiBridgeConfiguration config = new NukiBridgeConfiguration();
72
73     public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, @Nullable String callbackUrl) {
74         super(bridge);
75         logger.debug("Instantiating NukiBridgeHandler({}, {}, {})", bridge, httpClient, callbackUrl);
76         this.callbackUrl = callbackUrl;
77         this.httpClient = httpClient;
78     }
79
80     public @Nullable NukiHttpClient getNukiHttpClient() {
81         return this.nukiHttpClient;
82     }
83
84     public void withHttpClient(Consumer<NukiHttpClient> consumer) {
85         withHttpClient(client -> {
86             consumer.accept(client);
87             return null;
88         }, null);
89     }
90
91     protected <@Nullable U> @Nullable U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
92         NukiHttpClient client = this.nukiHttpClient;
93         if (client == null) {
94             logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
95                     new IllegalStateException());
96             return defaultValue;
97         } else {
98             return consumer.apply(client);
99         }
100     }
101
102     @Override
103     public void initialize() {
104         this.config = getConfigAs(NukiBridgeConfiguration.class);
105         String ip = config.ip;
106         Integer port = config.port;
107         String apiToken = config.apiToken;
108
109         if (ip == null || port == null) {
110             logger.debug("NukiBridgeHandler[{}] is not initializable, IP setting is unset in the configuration!",
111                     getThing().getUID());
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP setting is unset");
113         } else if (apiToken == null || apiToken.isBlank()) {
114             logger.debug("NukiBridgeHandler[{}] is not initializable, apiToken setting is unset in the configuration!",
115                     getThing().getUID());
116             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "apiToken setting is unset");
117         } else {
118             NukiLinkBuilder linkBuilder = new NukiLinkBuilder(ip, port, apiToken, this.config.secureToken);
119             nukiHttpClient = new NukiHttpClient(httpClient, linkBuilder);
120             scheduler.execute(this::initializeHandler);
121             checkBridgeOnlineJob = scheduler.scheduleWithFixedDelay(this::checkBridgeOnline, JOB_INTERVAL, JOB_INTERVAL,
122                     TimeUnit.SECONDS);
123         }
124     }
125
126     @Override
127     public void handleCommand(ChannelUID channelUID, Command command) {
128         logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, this.config.ip);
129     }
130
131     @Override
132     public void dispose() {
133         logger.debug("dispose() for Bridge[{}].", getThing().getUID());
134         if (this.config.manageCallbacks) {
135             unregisterCallback();
136         }
137         nukiHttpClient = null;
138         ScheduledFuture<?> job = checkBridgeOnlineJob;
139         if (job != null) {
140             job.cancel(true);
141         }
142         checkBridgeOnlineJob = null;
143     }
144
145     @Override
146     public Collection<Class<? extends ThingHandlerService>> getServices() {
147         return Set.of(NukiDeviceDiscoveryService.class);
148     }
149
150     private synchronized void initializeHandler() {
151         withHttpClient(client -> {
152             BridgeInfoResponse bridgeInfoResponse = client.getBridgeInfo();
153             if (bridgeInfoResponse.getStatus() == HttpStatus.OK_200) {
154                 updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, bridgeInfoResponse.getFirmwareVersion());
155                 updateProperty(NukiBindingConstants.PROPERTY_WIFI_FIRMWARE_VERSION,
156                         bridgeInfoResponse.getWifiFirmwareVersion());
157                 updateProperty(NukiBindingConstants.PROPERTY_HARDWARE_ID,
158                         Integer.toString(bridgeInfoResponse.getHardwareId()));
159                 updateProperty(NukiBindingConstants.PROPERTY_SERVER_ID,
160                         Integer.toString(bridgeInfoResponse.getServerId()));
161                 if (this.config.manageCallbacks) {
162                     manageNukiBridgeCallbacks();
163                 }
164                 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", this.config.ip,
165                         bridgeInfoResponse.getStatus());
166                 updateStatus(ThingStatus.ONLINE);
167             } else {
168                 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
169                         bridgeInfoResponse.getStatus());
170                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
171                         bridgeInfoResponse.getMessage());
172             }
173         });
174     }
175
176     public void checkBridgeOnline() {
177         logger.debug("checkBridgeOnline():bridgeIp[{}] status[{}]", this.config.ip, getThing().getStatus());
178         if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
179             withHttpClient(client -> {
180                 logger.debug("Requesting BridgeInfo to ensure Bridge[{}] is online.", this.config.ip);
181                 BridgeInfoResponse bridgeInfoResponse = client.getBridgeInfo();
182                 int status = bridgeInfoResponse.getStatus();
183                 if (status == HttpStatus.OK_200) {
184                     logger.debug("Bridge[{}] responded with status[{}]. Bridge is online.", this.config.ip, status);
185                 } else if (status == HttpStatus.SERVICE_UNAVAILABLE_503) {
186                     logger.debug(
187                             "Bridge[{}] responded with status[{}]. REST service seems to be busy but Bridge is online.",
188                             this.config.ip, status);
189                 } else {
190                     logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
191                             status);
192                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193                             bridgeInfoResponse.getMessage());
194                 }
195             });
196         } else {
197             initializeHandler();
198         }
199     }
200
201     private boolean isHttpClientNull() {
202         NukiHttpClient httpClient = getNukiHttpClient();
203         if (httpClient == null) {
204             logger.debug("HTTP Client not configured, switching bridge to OFFLINE");
205             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "HTTP Client not configured");
206             return true;
207         } else {
208             return false;
209         }
210     }
211
212     private @Nullable List<BridgeApiCallbackListCallbackDto> listCallbacks() {
213         if (isHttpClientNull()) {
214             return Collections.emptyList();
215         }
216
217         return withHttpClient(client -> {
218             BridgeCallbackListResponse bridgeCallbackListResponse = client.getBridgeCallbackList();
219             if (bridgeCallbackListResponse.isSuccess()) {
220                 return bridgeCallbackListResponse.getCallbacks();
221             } else {
222                 logger.debug("Failed to list callbacks for Bridge[{}] - status {}, message {}", this.config.ip,
223                         bridgeCallbackListResponse.getStatus(), bridgeCallbackListResponse.getMessage());
224                 return null;
225             }
226         }, null);
227     }
228
229     private void manageNukiBridgeCallbacks() {
230         String callback = callbackUrl;
231         if (callback == null) {
232             logger.debug("Cannot manage callbacks - no URL available");
233             return;
234         }
235
236         logger.debug("manageNukiBridgeCallbacks() for Bridge[{}].", this.config.ip);
237
238         List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
239         if (callbacks == null) {
240             return;
241         }
242
243         List<Integer> callbacksToRemove = new ArrayList<>(3);
244
245         // callback already registered - do nothing
246         if (callbacks.stream().anyMatch(cb -> cb.getUrl().equals(callback))) {
247             logger.debug("callbackUrl[{}] already existing on Bridge[{}].", callbackUrl, this.config.ip);
248             return;
249         }
250         // delete callbacks for this bridge registered for different host
251         String path = NukiLinkBuilder.callbackPath(getThing().getUID().getId()).build().toString();
252         callbacks.stream().filter(cb -> cb.getUrl().endsWith(path)).map(BridgeApiCallbackListCallbackDto::getId)
253                 .forEach(callbacksToRemove::add);
254         // delete callbacks for this bridge registered without bridgeId query (created by previous binding version)
255         String urlWithoutQuery = UriBuilder.fromUri(callback).replaceQuery("").build().toString();
256         callbacks.stream().filter(cb -> cb.getUrl().equals(urlWithoutQuery))
257                 .map(BridgeApiCallbackListCallbackDto::getId).forEach(callbacksToRemove::add);
258
259         if (callbacks.size() - callbacksToRemove.size() == 3) {
260             logger.debug("Already 3 callback URLs existing on Bridge[{}] - Removing ID 0!", this.config.ip);
261             callbacksToRemove.add(0);
262         }
263
264         callbacksToRemove.forEach(callbackId -> {
265             withHttpClient(client -> {
266                 BridgeCallbackRemoveResponse bridgeCallbackRemoveResponse = client.getBridgeCallbackRemove(callbackId);
267                 if (bridgeCallbackRemoveResponse.getStatus() == HttpStatus.OK_200) {
268                     logger.debug("Successfully removed callbackUrl[{}] on Bridge[{}]!", callback, this.config.ip);
269                 }
270             });
271         });
272
273         withHttpClient(client -> {
274             logger.debug("Adding callbackUrl[{}] to Bridge[{}]!", callbackUrl, this.config.ip);
275             BridgeCallbackAddResponse bridgeCallbackAddResponse = client.getBridgeCallbackAdd(callback);
276             if (bridgeCallbackAddResponse.getStatus() == HttpStatus.OK_200) {
277                 logger.debug("Successfully added callbackUrl[{}] on Bridge[{}]!", callback, this.config.ip);
278             }
279         });
280     }
281
282     private void unregisterCallback() {
283         List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
284         if (callbacks == null) {
285             return;
286         }
287
288         callbacks.stream().filter(callback -> callback.getUrl().equals(callbackUrl))
289                 .map(BridgeApiCallbackListCallbackDto::getId)
290                 .forEach(callbackId -> withHttpClient(client -> client.getBridgeCallbackRemove(callbackId)));
291     }
292 }