]> git.basschouten.com Git - openhab-addons.git/blob
587e041a7b4fedd8a7fc6e1c0c3a5725f4dcb57a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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
17 import java.util.List;
18 import java.util.Map;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import javax.measure.Unit;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.deconz.internal.Util;
27 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
28 import org.openhab.binding.deconz.internal.dto.SensorConfig;
29 import org.openhab.binding.deconz.internal.dto.SensorMessage;
30 import org.openhab.binding.deconz.internal.dto.SensorState;
31 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
32 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.thing.Channel;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.ThingHandlerCallback;
42 import org.openhab.core.thing.type.ChannelKind;
43 import org.openhab.core.thing.type.ChannelTypeUID;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.Gson;
49
50 /**
51  * This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
52  *
53  * It waits for the bridge to come online, grab the websocket connection and bridge configuration
54  * and registers to the websocket connection as a listener.
55  *
56  * A REST API call is made to get the initial sensor state.
57  *
58  * Every sensor and switch is supported by this Thing, because a unified state is kept
59  * in {@link #sensorState}. Every field that got received by the REST API for this specific
60  * sensor is published to the framework.
61  *
62  * @author David Graeff - Initial contribution
63  * @author Lukas Agethen - Refactored to provide better extensibility
64  */
65 @NonNullByDefault
66 public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<SensorMessage> {
67     private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
68     /**
69      * The sensor state. Contains all possible fields for all supported sensors and switches
70      */
71     protected SensorConfig sensorConfig = new SensorConfig();
72     protected SensorState sensorState = new SensorState();
73     /**
74      * Prevent a dispose/init cycle while this flag is set. Use for property updates
75      */
76     private boolean ignoreConfigurationUpdate;
77     private @Nullable ScheduledFuture<?> lastSeenPollingJob;
78
79     public SensorBaseThingHandler(Thing thing, Gson gson) {
80         super(thing, gson);
81     }
82
83     @Override
84     protected void requestState() {
85         requestState("sensors");
86     }
87
88     @Override
89     protected void registerListener() {
90         WebSocketConnection conn = connection;
91         if (conn != null) {
92             conn.registerSensorListener(config.id, this);
93         }
94     }
95
96     @Override
97     protected void unregisterListener() {
98         WebSocketConnection conn = connection;
99         if (conn != null) {
100             conn.unregisterSensorListener(config.id);
101         }
102     }
103
104     @Override
105     public void dispose() {
106         ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
107         if (lastSeenPollingJob != null) {
108             lastSeenPollingJob.cancel(true);
109             this.lastSeenPollingJob = null;
110         }
111
112         super.dispose();
113     }
114
115     @Override
116     public abstract void handleCommand(ChannelUID channelUID, Command command);
117
118     protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
119
120     protected abstract List<String> getConfigChannels();
121
122     @Override
123     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
124         if (!ignoreConfigurationUpdate) {
125             super.handleConfigurationUpdate(configurationParameters);
126         }
127     }
128
129     @Override
130     protected @Nullable SensorMessage parseStateResponse(AsyncHttpClient.Result r) {
131         if (r.getResponseCode() == 403) {
132             return null;
133         } else if (r.getResponseCode() == 200) {
134             return gson.fromJson(r.getBody(), SensorMessage.class);
135         } else {
136             throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
137         }
138     }
139
140     @Override
141     protected void processStateResponse(@Nullable SensorMessage stateResponse) {
142         logger.trace("{} received {}", thing.getUID(), stateResponse);
143         if (stateResponse == null) {
144             return;
145         }
146         SensorConfig newSensorConfig = stateResponse.config;
147         sensorConfig = newSensorConfig != null ? newSensorConfig : new SensorConfig();
148         SensorState newSensorState = stateResponse.state;
149         sensorState = newSensorState != null ? newSensorState : new SensorState();
150
151         // Add some information about the sensor
152         if (!sensorConfig.reachable) {
153             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
154             return;
155         }
156
157         if (!sensorConfig.on) {
158             updateStatus(ThingStatus.OFFLINE);
159             return;
160         }
161
162         Map<String, String> editProperties = editProperties();
163         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, stateResponse.swversion);
164         editProperties.put(Thing.PROPERTY_MODEL_ID, stateResponse.modelid);
165         editProperties.put(UNIQUE_ID, stateResponse.uniqueid);
166         ignoreConfigurationUpdate = true;
167         updateProperties(editProperties);
168
169         // Some sensors support optional channels
170         // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
171         // any battery-powered sensor
172         if (sensorConfig.battery != null) {
173             createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE);
174             createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
175         }
176
177         createTypeSpecificChannels(sensorConfig, sensorState);
178
179         ignoreConfigurationUpdate = false;
180
181         // Initial data
182         updateChannels(sensorConfig);
183         updateChannels(sensorState, true);
184
185         // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
186         // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
187         // So to monitor a sensor is still alive, the "last seen" is necessary.
188         String lastSeen = stateResponse.lastseen;
189         if (lastSeen != null && config.lastSeenPolling > 0) {
190             createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
191             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
192             // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
193             // manually poll it after the defined time (default is off)
194             if (config.lastSeenPolling > 0) {
195                 lastSeenPollingJob = scheduler.schedule((Runnable) this::requestState, config.lastSeenPolling,
196                         TimeUnit.MINUTES);
197                 logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
198                         config.lastSeenPolling);
199             }
200         }
201
202         updateStatus(ThingStatus.ONLINE);
203     }
204
205     protected void createChannel(String channelId, ChannelKind kind) {
206         ThingHandlerCallback callback = getCallback();
207         if (callback != null) {
208             ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
209             ChannelTypeUID channelTypeUID;
210             switch (channelId) {
211                 case CHANNEL_BATTERY_LEVEL:
212                     channelTypeUID = new ChannelTypeUID("system:battery-level");
213                     break;
214                 case CHANNEL_BATTERY_LOW:
215                     channelTypeUID = new ChannelTypeUID("system:low-battery");
216                     break;
217                 default:
218                     channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
219                     break;
220             }
221             Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
222             updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
223         }
224     }
225
226     /**
227      * Update channel value from {@link SensorConfig} object - override to include further channels
228      *
229      * @param channelUID
230      * @param newConfig
231      */
232     protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
233         Integer batteryLevel = newConfig.battery;
234         switch (channelUID.getId()) {
235             case CHANNEL_BATTERY_LEVEL:
236                 if (batteryLevel != null) {
237                     updateState(channelUID, new DecimalType(batteryLevel.longValue()));
238                 }
239                 break;
240             case CHANNEL_BATTERY_LOW:
241                 if (batteryLevel != null) {
242                     updateState(channelUID, OnOffType.from(batteryLevel <= 10));
243                 }
244                 break;
245             default:
246                 // other cases covered by sub-class
247         }
248     }
249
250     /**
251      * Update channel value from {@link SensorState} object - override to include further channels
252      *
253      * @param channelID
254      * @param newState
255      * @param initializing
256      */
257     protected void valueUpdated(String channelID, SensorState newState, boolean initializing) {
258         switch (channelID) {
259             case CHANNEL_LAST_UPDATED:
260                 String lastUpdated = newState.lastupdated;
261                 if (lastUpdated != null && !"none".equals(lastUpdated)) {
262                     updateState(channelID, Util.convertTimestampToDateTime(lastUpdated));
263                 }
264                 break;
265             default:
266                 // other cases covered by sub-class
267         }
268     }
269
270     @Override
271     public void messageReceived(String sensorID, DeconzBaseMessage message) {
272         logger.trace("{} received {}", thing.getUID(), message);
273         if (message instanceof SensorMessage) {
274             SensorMessage sensorMessage = (SensorMessage) message;
275             SensorConfig sensorConfig = sensorMessage.config;
276             if (sensorConfig != null) {
277                 this.sensorConfig = sensorConfig;
278                 updateChannels(sensorConfig);
279             }
280             SensorState sensorState = sensorMessage.state;
281             if (sensorState != null) {
282                 updateChannels(sensorState, false);
283             }
284         }
285     }
286
287     private void updateChannels(SensorConfig newConfig) {
288         List<String> configChannels = getConfigChannels();
289         thing.getChannels().stream().map(Channel::getUID)
290                 .filter(channelUID -> configChannels.contains(channelUID.getId()))
291                 .forEach((channelUID) -> valueUpdated(channelUID, newConfig));
292     }
293
294     protected void updateChannels(SensorState newState, boolean initializing) {
295         sensorState = newState;
296         thing.getChannels().forEach(channel -> valueUpdated(channel.getUID().getId(), newState, initializing));
297     }
298
299     protected void updateSwitchChannel(String channelID, @Nullable Boolean value) {
300         if (value == null) {
301             return;
302         }
303         updateState(channelID, OnOffType.from(value));
304     }
305
306     protected void updateDecimalTypeChannel(String channelID, @Nullable Number value) {
307         if (value == null) {
308             return;
309         }
310         updateState(channelID, new DecimalType(value.longValue()));
311     }
312
313     protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit) {
314         updateQuantityTypeChannel(channelID, value, unit, 1.0);
315     }
316
317     protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit, double scaling) {
318         if (value == null) {
319             return;
320         }
321         updateState(channelID, new QuantityType<>(value.doubleValue() * scaling, unit));
322     }
323 }