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