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.deconz.internal.handler;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19 import java.util.function.Consumer;
20 import java.util.stream.Collectors;
21 import java.util.stream.Stream;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
26 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
27 import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
28 import org.openhab.binding.deconz.internal.types.ResourceType;
29 import org.openhab.core.thing.Bridge;
30 import org.openhab.core.thing.Channel;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.ThingStatusInfo;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.thing.binding.ThingHandlerCallback;
38 import org.openhab.core.thing.type.ChannelKind;
39 import org.openhab.core.thing.type.ChannelTypeUID;
40 import org.openhab.core.types.Command;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
47 * This base thing doesn't establish any connections, that is done by the bridge Thing.
49 * It waits for the bridge to come online, grab the websocket connection and bridge configuration
50 * and registers to the websocket connection as a listener.
52 * @author David Graeff - Initial contribution
53 * @author Jan N. Klug - Refactored to abstract class
56 public abstract class DeconzBaseThingHandler extends BaseThingHandler implements WebSocketMessageListener {
57 private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
58 protected final ResourceType resourceType;
59 protected ThingConfig config = new ThingConfig();
60 protected final Gson gson;
61 private @Nullable ScheduledFuture<?> initializationJob;
62 protected @Nullable WebSocketConnection connection;
64 public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
67 this.resourceType = resourceType;
71 * Stops the API request
73 private void stopInitializationJob() {
74 ScheduledFuture<?> future = initializationJob;
77 initializationJob = null;
81 private void registerListener() {
82 WebSocketConnection conn = connection;
84 conn.registerListener(resourceType, config.id, this);
88 private void unregisterListener() {
89 WebSocketConnection conn = connection;
91 conn.unregisterListener(resourceType, config.id);
95 private @Nullable DeconzBridgeHandler getBridgeHandler() {
96 Bridge bridge = getBridge();
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
101 return (DeconzBridgeHandler) bridge.getHandler();
105 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
106 if (config.id.isEmpty()) {
107 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "ID not set");
111 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
112 // the bridge is ONLINE, we can communicate with the gateway, so we update the connection parameters and
113 // register the listener
114 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
115 if (bridgeHandler == null) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
120 final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
121 this.connection = webSocketConnection;
123 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
128 // get initial values
129 requestState(this::processStateResponse);
131 // if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
132 // set the thing status to OFFLINE
133 unregisterListener();
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
139 * processes a newly received (initial) state response
141 * MUST set the thing status!
143 * @param stateResponse
145 protected abstract void processStateResponse(DeconzBaseMessage stateResponse);
148 * Perform a request to the REST API for retrieving the full light state with all data and configuration.
150 protected void requestState(Consumer<DeconzBaseMessage> processor) {
151 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
152 if (bridgeHandler != null) {
153 bridgeHandler.getBridgeFullState()
154 .thenAccept(f -> f.map(s -> s.getMessage(resourceType, config.id)).ifPresentOrElse(message -> {
155 logger.trace("{} processing {}", thing.getUID(), message);
156 processor.accept(message);
158 if (initializationJob != null) {
159 stopInitializationJob();
160 initializationJob = scheduler.schedule(() -> requestState(this::processStateResponse), 10,
168 * sends a command to the bridge with the default command URL
170 * @param object must be serializable and contain the command
171 * @param originalCommand the original openHAB command (used for logging purposes)
172 * @param channelUID the channel that this command was send to (used for logging purposes)
173 * @param acceptProcessing additional processing after the command was successfully send (might be null)
175 protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
176 @Nullable Runnable acceptProcessing) {
177 sendCommand(object, originalCommand, channelUID, resourceType.getCommandUrl(), acceptProcessing);
181 * sends a command to the bridge with a caller-defined command URL
183 * @param object must be serializable and contain the command
184 * @param originalCommand the original openHAB command (used for logging purposes)
185 * @param channelUID the channel that this command was send to (used for logging purposes)
186 * @param commandUrl the command URL
187 * @param acceptProcessing additional processing after the command was successfully send (might be null)
189 protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
190 String commandUrl, @Nullable Runnable acceptProcessing) {
191 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
192 if (bridgeHandler == null) {
195 String endpoint = Stream.of(resourceType.getIdentifier(), config.id, commandUrl)
196 .collect(Collectors.joining("/"));
198 bridgeHandler.sendObject(endpoint, object).thenAccept(v -> {
199 if (acceptProcessing != null) {
200 acceptProcessing.run();
202 if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
203 logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID,
204 v.getResponseCode(), v.getBody());
206 logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
208 }).exceptionally(e -> {
209 logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID, e.getClass(),
216 public void dispose() {
217 stopInitializationJob();
218 unregisterListener();
223 public void initialize() {
224 config = getConfigAs(ThingConfig.class);
226 Bridge bridge = getBridge();
227 if (bridge != null) {
228 bridgeStatusChanged(bridge.getStatusInfo());
232 protected void createChannel(String channelId, ChannelKind kind) {
233 if (thing.getChannel(channelId) != null) {
234 // channel already exists, no update necessary
238 ThingHandlerCallback callback = getCallback();
239 if (callback != null) {
240 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
241 ChannelTypeUID channelTypeUID;
243 case CHANNEL_BATTERY_LEVEL:
244 channelTypeUID = new ChannelTypeUID("system:battery-level");
246 case CHANNEL_BATTERY_LOW:
247 channelTypeUID = new ChannelTypeUID("system:low-battery");
250 channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
253 Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
254 updateThing(editThing().withChannel(channel).build());