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;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.function.Consumer;
22 import java.util.function.Function;
24 import javax.ws.rs.core.UriBuilder;
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;
51 * The {@link NukiBridgeHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author Markus Katter - Initial contribution
55 * @contributer Jan Vybíral - Improved callback handling
58 public class NukiBridgeHandler extends BaseBridgeHandler {
60 private final Logger logger = LoggerFactory.getLogger(NukiBridgeHandler.class);
61 private static final int JOB_INTERVAL = 600;
63 private final HttpClient httpClient;
65 private NukiHttpClient nukiHttpClient;
67 private final String callbackUrl;
69 private ScheduledFuture<?> checkBridgeOnlineJob;
70 private NukiBridgeConfiguration config = new NukiBridgeConfiguration();
72 public NukiBridgeHandler(Bridge bridge, HttpClient httpClient, @Nullable String callbackUrl) {
74 logger.debug("Instantiating NukiBridgeHandler({}, {}, {})", bridge, httpClient, callbackUrl);
75 this.callbackUrl = callbackUrl;
76 this.httpClient = httpClient;
79 public @Nullable NukiHttpClient getNukiHttpClient() {
80 return this.nukiHttpClient;
83 public void withHttpClient(Consumer<NukiHttpClient> consumer) {
84 withHttpClient(client -> {
85 consumer.accept(client);
90 protected <@Nullable U> @Nullable U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
91 NukiHttpClient client = this.nukiHttpClient;
93 logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
94 new IllegalStateException());
97 return consumer.apply(client);
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;
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");
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,
126 public void handleCommand(ChannelUID channelUID, Command command) {
127 logger.debug("handleCommand({}, {}) for Bridge[{}] not implemented!", channelUID, command, this.config.ip);
131 public void dispose() {
132 logger.debug("dispose() for Bridge[{}].", getThing().getUID());
133 if (this.config.manageCallbacks) {
134 unregisterCallback();
136 nukiHttpClient = null;
137 ScheduledFuture<?> job = checkBridgeOnlineJob;
141 checkBridgeOnlineJob = null;
145 public Collection<Class<? extends ThingHandlerService>> getServices() {
146 return Collections.singleton(NukiDeviceDiscoveryService.class);
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();
163 logger.debug("Bridge[{}] responded with status[{}]. Switching the bridge online.", this.config.ip,
164 bridgeInfoResponse.getStatus());
165 updateStatus(ThingStatus.ONLINE);
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());
175 public void checkBridgeOnline() {
176 logger.debug("checkBridgeOnline():bridgeIp[{}] status[{}]", this.config.ip, getThing().getStatus());
177 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)));