]> git.basschouten.com Git - openhab-addons.git/blob
475fd1ec4f132ad7ca8b5cc6548a7678c5dd0ece
[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.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.function.Consumer;
22 import java.util.function.Function;
23
24 import javax.ws.rs.core.UriBuilder;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.http.HttpStatus;
30 import org.openhab.binding.nuki.internal.configuration.NukiBridgeConfiguration;
31 import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
32 import org.openhab.binding.nuki.internal.constants.NukiLinkBuilder;
33 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackAddResponse;
34 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackListResponse;
35 import org.openhab.binding.nuki.internal.dataexchange.BridgeCallbackRemoveResponse;
36 import org.openhab.binding.nuki.internal.dataexchange.BridgeInfoResponse;
37 import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
38 import org.openhab.binding.nuki.internal.discovery.NukiDeviceDiscoveryService;
39 import org.openhab.binding.nuki.internal.dto.BridgeApiCallbackListCallbackDto;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.openhab.core.types.Command;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * The {@link NukiBridgeHandler} is responsible for handling commands, which are
52  * sent to one of the channels.
53  *
54  * @author Markus Katter - Initial contribution
55  * @contributer Jan Vybíral - Improved callback handling
56  */
57 @NonNullByDefault
58 public class NukiBridgeHandler extends BaseBridgeHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(NukiBridgeHandler.class);
61     private static final int JOB_INTERVAL = 600;
62
63     private final HttpClient httpClient;
64     @Nullable
65     private NukiHttpClient nukiHttpClient;
66     @Nullable
67     private final String callbackUrl;
68     @Nullable
69     private ScheduledFuture<?> checkBridgeOnlineJob;
70     private NukiBridgeConfiguration config = new NukiBridgeConfiguration();
71
72     public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, @Nullable String callbackUrl) {
73         super(bridge);
74         logger.debug("Instantiating NukiBridgeHandler({}, {}, {})", bridge, httpClient, callbackUrl);
75         this.callbackUrl = callbackUrl;
76         this.httpClient = httpClient;
77     }
78
79     public @Nullable NukiHttpClient getNukiHttpClient() {
80         return this.nukiHttpClient;
81     }
82
83     public void withHttpClient(Consumer<NukiHttpClient> consumer) {
84         withHttpClient(client -> {
85             consumer.accept(client);
86             return null;
87         }, null);
88     }
89
90     protected <@Nullable U> @Nullable U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
91         NukiHttpClient client = this.nukiHttpClient;
92         if (client == null) {
93             logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
94                     new IllegalStateException());
95             return defaultValue;
96         } else {
97             return consumer.apply(client);
98         }
99     }
100
101     @Override
102     public void initialize() {
103         this.config = getConfigAs(NukiBridgeConfiguration.class);
104         String ip = config.ip;
105         Integer port = config.port;
106         String apiToken = config.apiToken;
107
108         if (ip == null || port == null) {
109             logger.debug("NukiBridgeHandler[{}] is not initializable, IP setting is unset in the configuration!",
110                     getThing().getUID());
111             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "IP setting is unset");
112         } else if (apiToken == null || apiToken.isBlank()) {
113             logger.debug("NukiBridgeHandler[{}] is not initializable, apiToken setting is unset in the configuration!",
114                     getThing().getUID());
115             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "apiToken setting is unset");
116         } else {
117             NukiLinkBuilder linkBuilder = new NukiLinkBuilder(ip, port, apiToken, this.config.secureToken);
118             nukiHttpClient = new NukiHttpClient(httpClient, linkBuilder);
119             scheduler.execute(this::initializeHandler);
120             checkBridgeOnlineJob = scheduler.scheduleWithFixedDelay(this::checkBridgeOnline, JOB_INTERVAL, JOB_INTERVAL,
121                     TimeUnit.SECONDS);
122         }
123     }
124
125     @Override
126     public void handleCommand(ChannelUID channelUID, Command command) {
127         logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, this.config.ip);
128     }
129
130     @Override
131     public void dispose() {
132         logger.debug("dispose() for Bridge[{}].", getThing().getUID());
133         if (this.config.manageCallbacks) {
134             unregisterCallback();
135         }
136         nukiHttpClient = null;
137         ScheduledFuture<?> job = checkBridgeOnlineJob;
138         if (job != null) {
139             job.cancel(true);
140         }
141         checkBridgeOnlineJob = null;
142     }
143
144     @Override
145     public Collection<Class<? extends ThingHandlerService>> getServices() {
146         return Collections.singleton(NukiDeviceDiscoveryService.class);
147     }
148
149     private synchronized void initializeHandler() {
150         withHttpClient(client -> {
151             BridgeInfoResponse bridgeInfoResponse = client.getBridgeInfo();
152             if (bridgeInfoResponse.getStatus() == HttpStatus.OK_200) {
153                 updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, bridgeInfoResponse.getFirmwareVersion());
154                 updateProperty(NukiBindingConstants.PROPERTY_WIFI_FIRMWARE_VERSION,
155                         bridgeInfoResponse.getWifiFirmwareVersion());
156                 updateProperty(NukiBindingConstants.PROPERTY_HARDWARE_ID,
157                         Integer.toString(bridgeInfoResponse.getHardwareId()));
158                 updateProperty(NukiBindingConstants.PROPERTY_SERVER_ID,
159                         Integer.toString(bridgeInfoResponse.getServerId()));
160                 if (this.config.manageCallbacks) {
161                     manageNukiBridgeCallbacks();
162                 }
163                 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", this.config.ip,
164                         bridgeInfoResponse.getStatus());
165                 updateStatus(ThingStatus.ONLINE);
166             } else {
167                 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
168                         bridgeInfoResponse.getStatus());
169                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
170                         bridgeInfoResponse.getMessage());
171             }
172         });
173     }
174
175     public void checkBridgeOnline() {
176         logger.debug("checkBridgeOnline():bridgeIp[{}] status[{}]", this.config.ip, getThing().getStatus());
177         if (getThing().getStatus().equals(ThingStatus.ONLINE)) {
178
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 }