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.nuki.internal.handler;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.function.Consumer;
23 import java.util.function.Function;
25 import javax.ws.rs.core.UriBuilder;
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;
52 * The {@link NukiBridgeHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Markus Katter - Initial contribution
56 * @contributer Jan Vybíral - Improved callback handling
59 public class NukiBridgeHandler extends BaseBridgeHandler {
61 private final Logger logger = LoggerFactory.getLogger(NukiBridgeHandler.class);
62 private static final int JOB_INTERVAL = 600;
64 private final HttpClient httpClient;
66 private NukiHttpClient nukiHttpClient;
68 private final String callbackUrl;
70 private ScheduledFuture<?> checkBridgeOnlineJob;
71 private NukiBridgeConfiguration config = new NukiBridgeConfiguration();
73 public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, @Nullable String callbackUrl) {
75 logger.debug("Instantiating NukiBridgeHandler({}, {}, {})", bridge, httpClient, callbackUrl);
76 this.callbackUrl = callbackUrl;
77 this.httpClient = httpClient;
80 public @Nullable NukiHttpClient getNukiHttpClient() {
81 return this.nukiHttpClient;
84 public void withHttpClient(Consumer<NukiHttpClient> consumer) {
85 withHttpClient(client -> {
86 consumer.accept(client);
91 protected <@Nullable U> @Nullable U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
92 NukiHttpClient client = this.nukiHttpClient;
94 logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
95 new IllegalStateException());
98 return consumer.apply(client);
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;
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");
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,
127 public void handleCommand(ChannelUID channelUID, Command command) {
128 logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, this.config.ip);
132 public void dispose() {
133 logger.debug("dispose() for Bridge[{}].", getThing().getUID());
134 if (this.config.manageCallbacks) {
135 unregisterCallback();
137 nukiHttpClient = null;
138 ScheduledFuture<?> job = checkBridgeOnlineJob;
142 checkBridgeOnlineJob = null;
146 public Collection<Class<? extends ThingHandlerService>> getServices() {
147 return Set.of(NukiDeviceDiscoveryService.class);
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();
164 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", this.config.ip,
165 bridgeInfoResponse.getStatus());
166 updateStatus(ThingStatus.ONLINE);
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());
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) {
187 "Bridge[{}] responded with status[{}]. REST service seems to be busy but Bridge is online.",
188 this.config.ip, status);
190 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge offline!", this.config.ip,
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193 bridgeInfoResponse.getMessage());
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");
212 private @Nullable List<BridgeApiCallbackListCallbackDto> listCallbacks() {
213 if (isHttpClientNull()) {
214 return Collections.emptyList();
217 return withHttpClient(client -> {
218 BridgeCallbackListResponse bridgeCallbackListResponse = client.getBridgeCallbackList();
219 if (bridgeCallbackListResponse.isSuccess()) {
220 return bridgeCallbackListResponse.getCallbacks();
222 logger.debug("Failed to list callbacks for Bridge[{}] - status {}, message {}", this.config.ip,
223 bridgeCallbackListResponse.getStatus(), bridgeCallbackListResponse.getMessage());
229 private void manageNukiBridgeCallbacks() {
230 String callback = callbackUrl;
231 if (callback == null) {
232 logger.debug("Cannot manage callbacks - no URL available");
236 logger.debug("manageNukiBridgeCallbacks() for Bridge[{}].", this.config.ip);
238 List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
239 if (callbacks == null) {
243 List<Integer> callbacksToRemove = new ArrayList<>(3);
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);
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);
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);
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);
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);
282 private void unregisterCallback() {
283 List<BridgeApiCallbackListCallbackDto> callbacks = listCallbacks();
284 if (callbacks == null) {
288 callbacks.stream().filter(callback -> callback.getUrl().equals(callbackUrl))
289 .map(BridgeApiCallbackListCallbackDto::getId)
290 .forEach(callbackId -> withHttpClient(client -> client.getBridgeCallbackRemove(callbackId)));