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.*;
16 import static org.openhab.binding.deconz.internal.Util.toPercentType;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.function.Consumer;
22 import javax.measure.Unit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.http.HttpMethod;
27 import org.openhab.binding.deconz.internal.Util;
28 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
29 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
30 import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
31 import org.openhab.binding.deconz.internal.types.ResourceType;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingStatusInfo;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandlerCallback;
45 import org.openhab.core.thing.binding.builder.ThingBuilder;
46 import org.openhab.core.thing.type.ChannelKind;
47 import org.openhab.core.thing.type.ChannelTypeUID;
48 import org.openhab.core.types.Command;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
55 * This base thing doesn't establish any connections, that is done by the bridge Thing.
57 * It waits for the bridge to come online, grab the websocket connection and bridge configuration
58 * and registers to the websocket connection as a listener.
60 * @author David Graeff - Initial contribution
61 * @author Jan N. Klug - Refactored to abstract class
64 public abstract class DeconzBaseThingHandler extends BaseThingHandler implements WebSocketMessageListener {
65 private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
66 protected final ResourceType resourceType;
67 protected ThingConfig config = new ThingConfig();
68 protected final Gson gson;
70 private @Nullable ScheduledFuture<?> initializationJob;
71 private @Nullable ScheduledFuture<?> lastSeenPollingJob;
72 protected @Nullable WebSocketConnection connection;
74 public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
77 this.resourceType = resourceType;
81 * Stops the initialization request
83 private void stopInitializationJob() {
84 ScheduledFuture<?> future = initializationJob;
87 initializationJob = null;
92 * Stops the last_seen polling
94 private void stopLastSeenPollingJob() {
95 ScheduledFuture<?> future = lastSeenPollingJob;
98 lastSeenPollingJob = null;
102 private void unregisterListener() {
103 WebSocketConnection conn = connection;
105 conn.unregisterListener(resourceType, config.id);
109 private @Nullable DeconzBridgeHandler getBridgeHandler() {
110 Bridge bridge = getBridge();
111 if (bridge == null) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
115 return (DeconzBridgeHandler) bridge.getHandler();
119 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
120 if (config.id.isEmpty()) {
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "ID not set");
125 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
126 // the bridge is ONLINE, we can communicate with the gateway, so we update the connection parameters and
127 // register the listener
128 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
129 if (bridgeHandler == null) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
134 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
137 WebSocketConnection socketConnection = bridgeHandler.getWebSocketConnection();
138 this.connection = socketConnection;
139 socketConnection.registerListener(resourceType, config.id, this);
141 // get initial values
142 requestState(this::processStateResponse);
144 // if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
145 // set the thing status to OFFLINE
146 unregisterListener();
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
152 * processes a newly received (initial) state response
154 * MUST set the thing status!
156 * @param stateResponse
158 protected abstract void processStateResponse(DeconzBaseMessage stateResponse);
161 * Perform a request to the REST API for retrieving the full state with all data and configuration.
163 protected void requestState(Consumer<DeconzBaseMessage> processor) {
164 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
165 if (bridgeHandler != null) {
166 bridgeHandler.getBridgeFullState()
167 .thenAccept(f -> f.map(s -> s.getMessage(resourceType, config.id)).ifPresentOrElse(message -> {
168 logger.trace("{} processing {}", thing.getUID(), message);
169 processor.accept(message);
171 if (initializationJob != null) {
172 stopInitializationJob();
173 initializationJob = scheduler.schedule(() -> requestState(this::processStateResponse), 10,
181 * create a channel on the current thing
183 * @param thingBuilder a ThingBuilder instance for this thing
184 * @param channelId the channel id
185 * @param kind the channel kind (STATE or TRIGGER)
186 * @return true if the thing was modified
188 protected boolean createChannel(ThingBuilder thingBuilder, String channelId, ChannelKind kind) {
189 if (thing.getChannel(channelId) != null) {
190 // channel already exists, no update necessary
194 ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
195 ChannelTypeUID channelTypeUID = switch (channelId) {
196 case CHANNEL_BATTERY_LEVEL -> new ChannelTypeUID("system:battery-level");
197 case CHANNEL_BATTERY_LOW -> new ChannelTypeUID("system:low-battery");
198 case CHANNEL_CONSUMPTION_2 -> new ChannelTypeUID("deconz:consumption");
199 default -> new ChannelTypeUID(BINDING_ID, channelId);
202 ThingHandlerCallback callback = getCallback();
203 if (callback != null) {
204 Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
205 thingBuilder.withChannel(channel);
206 logger.trace("Added '{}' to thing '{}'", channelId, thing.getUID());
211 logger.warn("Could not create channel '{}' for thing '{}'", channelUID, thing.getUID());
216 * check if we need to add a last seen channel (called from processStateResponse only)
218 * @param thingBuilder a ThingBuilder instance for this thing
219 * @param lastSeen the lastSeen string of a deconz message
220 * @return true if the thing was modified
222 protected boolean checkLastSeen(ThingBuilder thingBuilder, @Nullable String lastSeen) {
223 // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
224 // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
225 // So to monitor a sensor is still alive, the "last seen" is necessary.
226 // Because "last seen" is never updated by the WebSocket API we have to
227 // manually poll it after the defined time if supported by the device
228 stopLastSeenPollingJob();
229 boolean thingEdited = false;
230 if (lastSeen != null && config.lastSeenPolling > 0) {
231 thingEdited = createChannel(thingBuilder, CHANNEL_LAST_SEEN, ChannelKind.STATE);
232 updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
233 lastSeenPollingJob = scheduler.scheduleWithFixedDelay(() -> requestState(this::processLastSeen),
234 config.lastSeenPolling, config.lastSeenPolling, TimeUnit.MINUTES);
235 logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
236 config.lastSeenPolling);
237 } else if (thing.getChannel(CHANNEL_LAST_SEEN) != null) {
238 thingBuilder.withoutChannel(new ChannelUID(thing.getUID(), CHANNEL_LAST_SEEN));
245 private void processLastSeen(DeconzBaseMessage stateResponse) {
246 String lastSeen = stateResponse.lastseen;
247 if (lastSeen != null) {
248 updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
253 * sends a command to the bridge with the default command URL
255 * @param object must be serializable and contain the command
256 * @param originalCommand the original openHAB command (used for logging purposes)
257 * @param channelUID the channel that this command was sent to (used for logging purposes)
258 * @param acceptProcessing additional processing after the command was successfully send (might be null)
260 protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
261 @Nullable Runnable acceptProcessing) {
262 sendCommand(object, originalCommand, channelUID, resourceType.getCommandUrl(), acceptProcessing);
266 * sends a command to the bridge with a caller-defined command URL
268 * @param object must be serializable and contain the command
269 * @param originalCommand the original openHAB command (used for logging purposes)
270 * @param channelUID the channel that this command was sent to (used for logging purposes)
271 * @param commandUrl the command URL
272 * @param acceptProcessing additional processing after the command was successfully send (might be null)
274 protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
275 String commandUrl, @Nullable Runnable acceptProcessing) {
276 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
277 if (bridgeHandler == null) {
280 String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl);
282 bridgeHandler.sendObject(endpoint, object, HttpMethod.PUT).thenAccept(v -> {
283 if (acceptProcessing != null) {
284 acceptProcessing.run();
286 if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
287 logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID,
288 v.getResponseCode(), v.getBody());
290 logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
292 }).exceptionally(e -> {
293 logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID, e.getClass(),
299 public void doNetwork(@Nullable Object object, String commandUrl, HttpMethod httpMethod,
300 @Nullable Consumer<String> acceptProcessing) {
301 DeconzBridgeHandler bridgeHandler = getBridgeHandler();
302 if (bridgeHandler == null) {
305 String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl);
307 bridgeHandler.sendObject(endpoint, object, httpMethod).thenAccept(v -> {
308 if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
309 logger.warn("Sending {} via {} to {} failed: {} - {}", object, httpMethod, commandUrl,
310 v.getResponseCode(), v.getBody());
312 logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
313 if (acceptProcessing != null) {
314 acceptProcessing.accept(v.getBody());
317 }).exceptionally(e -> {
318 logger.warn("Sending {} via {} to {} failed: {} - {}", object, httpMethod, commandUrl, e.getClass(),
325 public void dispose() {
326 stopInitializationJob();
327 stopLastSeenPollingJob();
328 unregisterListener();
333 public void initialize() {
334 config = getConfigAs(ThingConfig.class);
336 Bridge bridge = getBridge();
337 if (bridge != null) {
338 bridgeStatusChanged(bridge.getStatusInfo());
342 protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
346 updateState(channelUID, new StringType(value));
349 protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
353 updateState(channelUID, OnOffType.from(value));
356 protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
360 updateState(channelUID, new DecimalType(value.longValue()));
363 protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
364 updateQuantityTypeChannel(channelUID, value, unit, 1.0);
367 protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit,
372 updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit));
376 * Update a channel with a {@link org.openhab.core.library.types.PercentType} of {@link OnOffType}
378 * If either {@param value} or {@param on} are <code>null</code> or {@param on} is <code>false</code> the method
379 * updated the channel with {@link OnOffType#OFF}, otherwise {@param value} is scaled and converted to
380 * {@link org.openhab.core.library.types.PercentType} before updating the channel.
382 * @param channelUID the {@link ChannelUID} that shall receive the update
383 * @param value an {@link Integer} value (0-255) that is posted
384 * @param on the on state of the channel
386 protected void updatePercentTypeChannel(ChannelUID channelUID, @Nullable Integer value, @Nullable Boolean on) {
387 if (value != null && on != null && on) {
388 updateState(channelUID, toPercentType(value));
390 updateState(channelUID, OnOffType.OFF);