]> git.basschouten.com Git - openhab-addons.git/blob
389d8c83f69c8ed5d5681892f7e8d2f56250a54b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.Objects;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.measure.Unit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.deconz.internal.Util;
28 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
29 import org.openhab.binding.deconz.internal.dto.SensorConfig;
30 import org.openhab.binding.deconz.internal.dto.SensorMessage;
31 import org.openhab.binding.deconz.internal.dto.SensorState;
32 import org.openhab.binding.deconz.internal.types.ResourceType;
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 {
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, ResourceType.SENSORS);
81     }
82
83     @Override
84     public void dispose() {
85         ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
86         if (lastSeenPollingJob != null) {
87             lastSeenPollingJob.cancel(true);
88             this.lastSeenPollingJob = null;
89         }
90
91         super.dispose();
92     }
93
94     @Override
95     public abstract void handleCommand(ChannelUID channelUID, Command command);
96
97     protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
98
99     protected abstract List<String> getConfigChannels();
100
101     @Override
102     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
103         if (!ignoreConfigurationUpdate) {
104             super.handleConfigurationUpdate(configurationParameters);
105         }
106     }
107
108     @Override
109     protected void processStateResponse(DeconzBaseMessage stateResponse) {
110         if (!(stateResponse instanceof SensorMessage)) {
111             return;
112         }
113
114         SensorMessage sensorMessage = (SensorMessage) stateResponse;
115         sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
116         sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
117
118         // Add some information about the sensor
119         if (!sensorConfig.reachable) {
120             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
121             return;
122         }
123
124         Map<String, String> editProperties = editProperties();
125         editProperties.put(UNIQUE_ID, sensorMessage.uniqueid);
126         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, sensorMessage.swversion);
127         editProperties.put(Thing.PROPERTY_VENDOR, sensorMessage.manufacturername);
128         editProperties.put(Thing.PROPERTY_MODEL_ID, sensorMessage.modelid);
129
130         ignoreConfigurationUpdate = true;
131
132         updateProperties(editProperties);
133
134         // Some sensors support optional channels
135         // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
136         // any battery-powered sensor
137         if (sensorConfig.battery != null) {
138             createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE);
139             createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
140         }
141
142         createTypeSpecificChannels(sensorConfig, sensorState);
143
144         ignoreConfigurationUpdate = false;
145
146         // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
147         // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
148         // So to monitor a sensor is still alive, the "last seen" is necessary.
149         // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
150         // manually poll it after the defined time
151         String lastSeen = sensorMessage.lastseen;
152         if (lastSeen != null && config.lastSeenPolling > 0) {
153             createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
154             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
155             lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
156                     TimeUnit.MINUTES);
157             logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
158                     config.lastSeenPolling);
159         }
160
161         // Initial data
162         updateChannels(sensorConfig);
163         updateChannels(sensorState, true);
164
165         updateStatus(ThingStatus.ONLINE);
166     }
167
168     private void processLastSeen(DeconzBaseMessage stateResponse) {
169         String lastSeen = stateResponse.lastseen;
170         if (lastSeen != null) {
171             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
172         }
173     }
174
175     protected void createChannel(String channelId, ChannelKind kind) {
176         if (thing.getChannel(channelId) != null) {
177             // channel already exists, no update necessary
178             return;
179         }
180
181         ThingHandlerCallback callback = getCallback();
182         if (callback != null) {
183             ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
184             ChannelTypeUID channelTypeUID;
185             switch (channelId) {
186                 case CHANNEL_BATTERY_LEVEL:
187                     channelTypeUID = new ChannelTypeUID("system:battery-level");
188                     break;
189                 case CHANNEL_BATTERY_LOW:
190                     channelTypeUID = new ChannelTypeUID("system:low-battery");
191                     break;
192                 default:
193                     channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
194                     break;
195             }
196             Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
197             updateThing(editThing().withChannel(channel).build());
198         }
199     }
200
201     /**
202      * Update channel value from {@link SensorConfig} object - override to include further channels
203      *
204      * @param channelUID
205      * @param newConfig
206      */
207     protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
208         Integer batteryLevel = newConfig.battery;
209         switch (channelUID.getId()) {
210             case CHANNEL_BATTERY_LEVEL:
211                 if (batteryLevel != null) {
212                     updateState(channelUID, new DecimalType(batteryLevel.longValue()));
213                 }
214                 break;
215             case CHANNEL_BATTERY_LOW:
216                 if (batteryLevel != null) {
217                     updateState(channelUID, OnOffType.from(batteryLevel <= 10));
218                 }
219                 break;
220             default:
221                 // other cases covered by sub-class
222         }
223     }
224
225     /**
226      * Update channel value from {@link SensorState} object - override to include further channels
227      *
228      * @param channelUID
229      * @param newState
230      * @param initializing
231      */
232     protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
233         switch (channelUID.getId()) {
234             case CHANNEL_LAST_UPDATED:
235                 String lastUpdated = newState.lastupdated;
236                 if (lastUpdated != null && !"none".equals(lastUpdated)) {
237                     updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated));
238                 }
239                 break;
240             default:
241                 // other cases covered by sub-class
242         }
243     }
244
245     @Override
246     public void messageReceived(String sensorID, DeconzBaseMessage message) {
247         logger.trace("{} received {}", thing.getUID(), message);
248         if (message instanceof SensorMessage) {
249             SensorMessage sensorMessage = (SensorMessage) message;
250             SensorConfig sensorConfig = sensorMessage.config;
251             if (sensorConfig != null) {
252                 this.sensorConfig = sensorConfig;
253                 updateChannels(sensorConfig);
254             }
255             SensorState sensorState = sensorMessage.state;
256             if (sensorState != null) {
257                 updateChannels(sensorState, false);
258             }
259         }
260     }
261
262     private void updateChannels(SensorConfig newConfig) {
263         List<String> configChannels = getConfigChannels();
264         thing.getChannels().stream().map(Channel::getUID)
265                 .filter(channelUID -> configChannels.contains(channelUID.getId()))
266                 .forEach((channelUID) -> valueUpdated(channelUID, newConfig));
267     }
268
269     protected void updateChannels(SensorState newState, boolean initializing) {
270         sensorState = newState;
271         thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing));
272     }
273
274     protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
275         if (value == null) {
276             return;
277         }
278         updateState(channelUID, OnOffType.from(value));
279     }
280
281     protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
282         if (value == null) {
283             return;
284         }
285         updateState(channelUID, new DecimalType(value.longValue()));
286     }
287
288     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
289         updateQuantityTypeChannel(channelUID, value, unit, 1.0);
290     }
291
292     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit,
293             double scaling) {
294         if (value == null) {
295             return;
296         }
297         updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit));
298     }
299 }