]> git.basschouten.com Git - openhab-addons.git/blob
58765a0aa1bc06339fda9dfe18eaab73cb8a0739
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.library.types.StringType;
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.type.ChannelKind;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import com.google.gson.Gson;
48
49 /**
50  * This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
51  *
52  * It waits for the bridge to come online, grab the websocket connection and bridge configuration
53  * and registers to the websocket connection as a listener.
54  *
55  * A REST API call is made to get the initial sensor state.
56  *
57  * Every sensor and switch is supported by this Thing, because a unified state is kept
58  * in {@link #sensorState}. Every field that got received by the REST API for this specific
59  * sensor is published to the framework.
60  *
61  * @author David Graeff - Initial contribution
62  * @author Lukas Agethen - Refactored to provide better extensibility
63  */
64 @NonNullByDefault
65 public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
66     private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
67     /**
68      * The sensor state. Contains all possible fields for all supported sensors and switches
69      */
70     protected SensorConfig sensorConfig = new SensorConfig();
71     protected SensorState sensorState = new SensorState();
72     /**
73      * Prevent a dispose/init cycle while this flag is set. Use for property updates
74      */
75     private boolean ignoreConfigurationUpdate;
76     private @Nullable ScheduledFuture<?> lastSeenPollingJob;
77
78     public SensorBaseThingHandler(Thing thing, Gson gson) {
79         super(thing, gson, ResourceType.SENSORS);
80     }
81
82     @Override
83     public void dispose() {
84         ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
85         if (lastSeenPollingJob != null) {
86             lastSeenPollingJob.cancel(true);
87             this.lastSeenPollingJob = null;
88         }
89
90         super.dispose();
91     }
92
93     @Override
94     public abstract void handleCommand(ChannelUID channelUID, Command command);
95
96     protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
97
98     protected abstract List<String> getConfigChannels();
99
100     @Override
101     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
102         if (!ignoreConfigurationUpdate) {
103             super.handleConfigurationUpdate(configurationParameters);
104         }
105     }
106
107     @Override
108     protected void processStateResponse(DeconzBaseMessage stateResponse) {
109         if (!(stateResponse instanceof SensorMessage)) {
110             return;
111         }
112
113         SensorMessage sensorMessage = (SensorMessage) stateResponse;
114         sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
115         sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
116
117         // Add some information about the sensor
118         if (!sensorConfig.reachable) {
119             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
120             return;
121         }
122
123         Map<String, String> editProperties = editProperties();
124         editProperties.put(UNIQUE_ID, sensorMessage.uniqueid);
125         editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, sensorMessage.swversion);
126         editProperties.put(Thing.PROPERTY_VENDOR, sensorMessage.manufacturername);
127         editProperties.put(Thing.PROPERTY_MODEL_ID, sensorMessage.modelid);
128
129         ignoreConfigurationUpdate = true;
130
131         updateProperties(editProperties);
132
133         // Some sensors support optional channels
134         // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
135         // any battery-powered sensor
136         if (sensorConfig.battery != null) {
137             createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE);
138             createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
139         }
140
141         createTypeSpecificChannels(sensorConfig, sensorState);
142
143         ignoreConfigurationUpdate = false;
144
145         // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
146         // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
147         // So to monitor a sensor is still alive, the "last seen" is necessary.
148         // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
149         // manually poll it after the defined time
150         String lastSeen = sensorMessage.lastseen;
151         if (lastSeen != null && config.lastSeenPolling > 0) {
152             createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
153             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
154             lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
155                     TimeUnit.MINUTES);
156             logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
157                     config.lastSeenPolling);
158         }
159
160         // Initial data
161         updateChannels(sensorConfig);
162         updateChannels(sensorState, true);
163
164         updateStatus(ThingStatus.ONLINE);
165     }
166
167     private void processLastSeen(DeconzBaseMessage stateResponse) {
168         String lastSeen = stateResponse.lastseen;
169         if (lastSeen != null) {
170             updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
171         }
172     }
173
174     /**
175      * Update channel value from {@link SensorConfig} object - override to include further channels
176      *
177      * @param channelUID
178      * @param newConfig
179      */
180     protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
181         Integer batteryLevel = newConfig.battery;
182         switch (channelUID.getId()) {
183             case CHANNEL_BATTERY_LEVEL:
184                 if (batteryLevel != null) {
185                     updateState(channelUID, new DecimalType(batteryLevel.longValue()));
186                 }
187                 break;
188             case CHANNEL_BATTERY_LOW:
189                 if (batteryLevel != null) {
190                     updateState(channelUID, OnOffType.from(batteryLevel <= 10));
191                 }
192                 break;
193             default:
194                 // other cases covered by sub-class
195         }
196     }
197
198     /**
199      * Update channel value from {@link SensorState} object - override to include further channels
200      *
201      * @param channelUID
202      * @param newState
203      * @param initializing
204      */
205     protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
206         switch (channelUID.getId()) {
207             case CHANNEL_LAST_UPDATED:
208                 String lastUpdated = newState.lastupdated;
209                 if (lastUpdated != null && !"none".equals(lastUpdated)) {
210                     updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated));
211                 }
212                 break;
213             default:
214                 // other cases covered by sub-class
215         }
216     }
217
218     @Override
219     public void messageReceived(String sensorID, DeconzBaseMessage message) {
220         logger.trace("{} received {}", thing.getUID(), message);
221         if (message instanceof SensorMessage) {
222             SensorMessage sensorMessage = (SensorMessage) message;
223             SensorConfig sensorConfig = sensorMessage.config;
224             if (sensorConfig != null) {
225                 this.sensorConfig = sensorConfig;
226                 updateChannels(sensorConfig);
227             }
228             SensorState sensorState = sensorMessage.state;
229             if (sensorState != null) {
230                 updateChannels(sensorState, false);
231             }
232         }
233     }
234
235     private void updateChannels(SensorConfig newConfig) {
236         List<String> configChannels = getConfigChannels();
237         thing.getChannels().stream().map(Channel::getUID)
238                 .filter(channelUID -> configChannels.contains(channelUID.getId()))
239                 .forEach((channelUID) -> valueUpdated(channelUID, newConfig));
240     }
241
242     protected void updateChannels(SensorState newState, boolean initializing) {
243         sensorState = newState;
244         thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing));
245     }
246
247     protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
248         if (value == null) {
249             return;
250         }
251         updateState(channelUID, OnOffType.from(value));
252     }
253
254     protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
255         updateState(channelUID, new StringType(value));
256     }
257
258     protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
259         if (value == null) {
260             return;
261         }
262         updateState(channelUID, new DecimalType(value.longValue()));
263     }
264
265     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
266         updateQuantityTypeChannel(channelUID, value, unit, 1.0);
267     }
268
269     protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit,
270             double scaling) {
271         if (value == null) {
272             return;
273         }
274         updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit));
275     }
276 }