2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.deconz.internal.handler;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
17 import java.util.List;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import javax.measure.Unit;
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;
48 import com.google.gson.Gson;
51 * This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
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.
56 * A REST API call is made to get the initial sensor state.
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.
62 * @author David Graeff - Initial contribution
63 * @author Lukas Agethen - Refactored to provide better extensibility
66 public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<SensorMessage> {
67 private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
69 * The sensor state. Contains all possible fields for all supported sensors and switches
71 protected SensorConfig sensorConfig = new SensorConfig();
72 protected SensorState sensorState = new SensorState();
74 * Prevent a dispose/init cycle while this flag is set. Use for property updates
76 private boolean ignoreConfigurationUpdate;
77 private @Nullable ScheduledFuture<?> lastSeenPollingJob;
79 public SensorBaseThingHandler(Thing thing, Gson gson) {
84 protected void requestState() {
85 requestState("sensors");
89 protected void registerListener() {
90 WebSocketConnection conn = connection;
92 conn.registerSensorListener(config.id, this);
97 protected void unregisterListener() {
98 WebSocketConnection conn = connection;
100 conn.unregisterSensorListener(config.id);
105 public void dispose() {
106 ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
107 if (lastSeenPollingJob != null) {
108 lastSeenPollingJob.cancel(true);
109 this.lastSeenPollingJob = null;
116 public abstract void handleCommand(ChannelUID channelUID, Command command);
118 protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
120 protected abstract List<String> getConfigChannels();
123 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
124 if (!ignoreConfigurationUpdate) {
125 super.handleConfigurationUpdate(configurationParameters);
130 protected @Nullable SensorMessage parseStateResponse(AsyncHttpClient.Result r) {
131 if (r.getResponseCode() == 403) {
133 } else if (r.getResponseCode() == 200) {
134 return gson.fromJson(r.getBody(), SensorMessage.class);
136 throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
141 protected void processStateResponse(@Nullable SensorMessage stateResponse) {
142 logger.trace("{} received {}", thing.getUID(), stateResponse);
143 if (stateResponse == null) {
146 SensorConfig newSensorConfig = stateResponse.config;
147 sensorConfig = newSensorConfig != null ? newSensorConfig : new SensorConfig();
148 SensorState newSensorState = stateResponse.state;
149 sensorState = newSensorState != null ? newSensorState : new SensorState();
151 // Add some information about the sensor
152 if (!sensorConfig.reachable) {
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
157 if (!sensorConfig.on) {
158 updateStatus(ThingStatus.OFFLINE);
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);
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);
177 createTypeSpecificChannels(sensorConfig, sensorState);
179 ignoreConfigurationUpdate = false;
182 updateChannels(sensorConfig);
183 updateChannels(sensorState, true);
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,
197 logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
198 config.lastSeenPolling);
202 updateStatus(ThingStatus.ONLINE);
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;
211 case CHANNEL_BATTERY_LEVEL:
212 channelTypeUID = new ChannelTypeUID("system:battery-level");
214 case CHANNEL_BATTERY_LOW:
215 channelTypeUID = new ChannelTypeUID("system:low-battery");
218 channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
221 Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
222 updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
227 * Update channel value from {@link SensorConfig} object - override to include further channels
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()));
240 case CHANNEL_BATTERY_LOW:
241 if (batteryLevel != null) {
242 updateState(channelUID, OnOffType.from(batteryLevel <= 10));
246 // other cases covered by sub-class
251 * Update channel value from {@link SensorState} object - override to include further channels
255 * @param initializing
257 protected void valueUpdated(String channelID, SensorState newState, boolean initializing) {
259 case CHANNEL_LAST_UPDATED:
260 String lastUpdated = newState.lastupdated;
261 if (lastUpdated != null && !"none".equals(lastUpdated)) {
262 updateState(channelID, Util.convertTimestampToDateTime(lastUpdated));
266 // other cases covered by sub-class
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);
280 SensorState sensorState = sensorMessage.state;
281 if (sensorState != null) {
282 updateChannels(sensorState, false);
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));
294 protected void updateChannels(SensorState newState, boolean initializing) {
295 sensorState = newState;
296 thing.getChannels().forEach(channel -> valueUpdated(channel.getUID().getId(), newState, initializing));
299 protected void updateSwitchChannel(String channelID, @Nullable Boolean value) {
303 updateState(channelID, OnOffType.from(value));
306 protected void updateDecimalTypeChannel(String channelID, @Nullable Number value) {
310 updateState(channelID, new DecimalType(value.longValue()));
313 protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit) {
314 updateQuantityTypeChannel(channelID, value, unit, 1.0);
317 protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit, double scaling) {
321 updateState(channelID, new QuantityType<>(value.doubleValue() * scaling, unit));