]> git.basschouten.com Git - openhab-addons.git/blob
7ae7a6cfa8265a41cfc31f040eb602fcb82d1a71
[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.deconz.internal.handler;
14
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.toPercentType;
17
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20 import java.util.function.Consumer;
21
22 import javax.measure.Unit;
23
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;
51
52 import com.google.gson.Gson;
53
54 /**
55  * This base thing doesn't establish any connections, that is done by the bridge Thing.
56  *
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.
59  **
60  * @author David Graeff - Initial contribution
61  * @author Jan N. Klug - Refactored to abstract class
62  */
63 @NonNullByDefault
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;
69
70     private @Nullable ScheduledFuture<?> initializationJob;
71     private @Nullable ScheduledFuture<?> lastSeenPollingJob;
72     protected @Nullable WebSocketConnection connection;
73
74     public DeconzBaseThingHandler(Thing thing, Gson gson, ResourceType resourceType) {
75         super(thing);
76         this.gson = gson;
77         this.resourceType = resourceType;
78     }
79
80     /**
81      * Stops the initialization request
82      */
83     private void stopInitializationJob() {
84         ScheduledFuture<?> future = initializationJob;
85         if (future != null) {
86             future.cancel(true);
87             initializationJob = null;
88         }
89     }
90
91     /**
92      * Stops the last_seen polling
93      */
94     private void stopLastSeenPollingJob() {
95         ScheduledFuture<?> future = lastSeenPollingJob;
96         if (future != null) {
97             future.cancel(true);
98             lastSeenPollingJob = null;
99         }
100     }
101
102     private void unregisterListener() {
103         WebSocketConnection conn = connection;
104         if (conn != null) {
105             conn.unregisterListener(resourceType, config.id);
106         }
107     }
108
109     private @Nullable DeconzBridgeHandler getBridgeHandler() {
110         Bridge bridge = getBridge();
111         if (bridge == null) {
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
113             return null;
114         }
115         return (DeconzBridgeHandler) bridge.getHandler();
116     }
117
118     @Override
119     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
120         if (config.id.isEmpty()) {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "ID not set");
122             return;
123         }
124
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);
131                 return;
132             }
133
134             updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
135
136             // Real-time data
137             WebSocketConnection socketConnection = bridgeHandler.getWebSocketConnection();
138             this.connection = socketConnection;
139             socketConnection.registerListener(resourceType, config.id, this);
140
141             // get initial values
142             requestState(this::processStateResponse);
143         } else {
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);
148         }
149     }
150
151     /**
152      * processes a newly received (initial) state response
153      *
154      * MUST set the thing status!
155      *
156      * @param stateResponse
157      */
158     protected abstract void processStateResponse(DeconzBaseMessage stateResponse);
159
160     /**
161      * Perform a request to the REST API for retrieving the full state with all data and configuration.
162      */
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);
170                     }, () -> {
171                         if (initializationJob != null) {
172                             stopInitializationJob();
173                             initializationJob = scheduler.schedule(() -> requestState(this::processStateResponse), 10,
174                                     TimeUnit.SECONDS);
175                         }
176                     }));
177         }
178     }
179
180     /**
181      * create a channel on the current thing
182      *
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
187      */
188     protected boolean createChannel(ThingBuilder thingBuilder, String channelId, ChannelKind kind) {
189         if (thing.getChannel(channelId) != null) {
190             // channel already exists, no update necessary
191             return false;
192         }
193
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);
200         };
201
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());
207
208             return true;
209         }
210
211         logger.warn("Could not create channel '{}' for thing '{}'", channelUID, thing.getUID());
212         return false;
213     }
214
215     /**
216      * check if we need to add a last seen channel (called from processStateResponse only)
217      *
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
221      */
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));
239             thingEdited = true;
240         }
241
242         return thingEdited;
243     }
244
245     private void processLastSeen(DeconzBaseMessage stateResponse) {
246         String lastSeen = stateResponse.lastseen;
247         if (lastSeen != null) {
248             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
249         }
250     }
251
252     /**
253      * sends a command to the bridge with the default command URL
254      *
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)
259      */
260     protected void sendCommand(@Nullable Object object, Command originalCommand, ChannelUID channelUID,
261             @Nullable Runnable acceptProcessing) {
262         sendCommand(object, originalCommand, channelUID, resourceType.getCommandUrl(), acceptProcessing);
263     }
264
265     /**
266      * sends a command to the bridge with a caller-defined command URL
267      *
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)
273      */
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) {
278             return;
279         }
280         String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl);
281
282         bridgeHandler.sendObject(endpoint, object, HttpMethod.PUT).thenAccept(v -> {
283             if (acceptProcessing != null) {
284                 acceptProcessing.run();
285             }
286             if (v.getResponseCode() != java.net.HttpURLConnection.HTTP_OK) {
287                 logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID,
288                         v.getResponseCode(), v.getBody());
289             } else {
290                 logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
291             }
292         }).exceptionally(e -> {
293             logger.warn("Sending command {} to channel {} failed: {} - {}", originalCommand, channelUID, e.getClass(),
294                     e.getMessage());
295             return null;
296         });
297     }
298
299     public void doNetwork(@Nullable Object object, String commandUrl, HttpMethod httpMethod,
300             @Nullable Consumer<String> acceptProcessing) {
301         DeconzBridgeHandler bridgeHandler = getBridgeHandler();
302         if (bridgeHandler == null) {
303             return;
304         }
305         String endpoint = String.join("/", resourceType.getIdentifier(), config.id, commandUrl);
306
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());
311             } else {
312                 logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
313                 if (acceptProcessing != null) {
314                     acceptProcessing.accept(v.getBody());
315                 }
316             }
317         }).exceptionally(e -> {
318             logger.warn("Sending {} via {} to {} failed: {} - {}", object, httpMethod, commandUrl, e.getClass(),
319                     e.getMessage());
320             return null;
321         });
322     }
323
324     @Override
325     public void dispose() {
326         stopInitializationJob();
327         stopLastSeenPollingJob();
328         unregisterListener();
329         super.dispose();
330     }
331
332     @Override
333     public void initialize() {
334         config = getConfigAs(ThingConfig.class);
335
336         Bridge bridge = getBridge();
337         if (bridge != null) {
338             bridgeStatusChanged(bridge.getStatusInfo());
339         }
340     }
341
342     protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
343         if (value == null) {
344             return;
345         }
346         updateState(channelUID, new StringType(value));
347     }
348
349     protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
350         if (value == null) {
351             return;
352         }
353         updateState(channelUID, OnOffType.from(value));
354     }
355
356     protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
357         if (value == null) {
358             return;
359         }
360         updateState(channelUID, new DecimalType(value.longValue()));
361     }
362
363     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
364         updateQuantityTypeChannel(channelUID, value, unit, 1.0);
365     }
366
367     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit,
368             double scaling) {
369         if (value == null) {
370             return;
371         }
372         updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit));
373     }
374
375     /**
376      * Update a channel with a {@link org.openhab.core.library.types.PercentType} of {@link OnOffType}
377      *
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.
381      *
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
385      */
386     protected void updatePercentTypeChannel(ChannelUID channelUID, @Nullable Integer value, @Nullable Boolean on) {
387         if (value != null && on != null && on) {
388             updateState(channelUID, toPercentType(value));
389         } else {
390             updateState(channelUID, OnOffType.OFF);
391         }
392     }
393 }