2 * Copyright (c) 2010-2023 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.Objects;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import javax.measure.Unit;
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;
47 import com.google.gson.Gson;
50 * This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
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.
55 * A REST API call is made to get the initial sensor state.
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.
61 * @author David Graeff - Initial contribution
62 * @author Lukas Agethen - Refactored to provide better extensibility
65 public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
66 private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
68 * The sensor state. Contains all possible fields for all supported sensors and switches
70 protected SensorConfig sensorConfig = new SensorConfig();
71 protected SensorState sensorState = new SensorState();
73 * Prevent a dispose/init cycle while this flag is set. Use for property updates
75 private boolean ignoreConfigurationUpdate;
76 private @Nullable ScheduledFuture<?> lastSeenPollingJob;
78 public SensorBaseThingHandler(Thing thing, Gson gson) {
79 super(thing, gson, ResourceType.SENSORS);
83 public void dispose() {
84 ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
85 if (lastSeenPollingJob != null) {
86 lastSeenPollingJob.cancel(true);
87 this.lastSeenPollingJob = null;
94 public abstract void handleCommand(ChannelUID channelUID, Command command);
96 protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
98 protected abstract List<String> getConfigChannels();
101 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
102 if (!ignoreConfigurationUpdate) {
103 super.handleConfigurationUpdate(configurationParameters);
108 protected void processStateResponse(DeconzBaseMessage stateResponse) {
109 if (!(stateResponse instanceof SensorMessage)) {
113 SensorMessage sensorMessage = (SensorMessage) stateResponse;
114 sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
115 sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
117 // Add some information about the sensor
118 if (!sensorConfig.reachable) {
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
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);
129 ignoreConfigurationUpdate = true;
131 updateProperties(editProperties);
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);
141 if (sensorState.lowbattery != null) {
142 createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
145 createTypeSpecificChannels(sensorConfig, sensorState);
147 ignoreConfigurationUpdate = false;
149 // "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
150 // For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
151 // So to monitor a sensor is still alive, the "last seen" is necessary.
152 // Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
153 // manually poll it after the defined time
154 String lastSeen = sensorMessage.lastseen;
155 if (lastSeen != null && config.lastSeenPolling > 0) {
156 createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
157 updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
158 lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
160 logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
161 config.lastSeenPolling);
165 updateChannels(sensorConfig);
166 updateChannels(sensorState, true);
168 updateStatus(ThingStatus.ONLINE);
171 private void processLastSeen(DeconzBaseMessage stateResponse) {
172 String lastSeen = stateResponse.lastseen;
173 if (lastSeen != null) {
174 updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
179 * Update channel value from {@link SensorConfig} object - override to include further channels
184 protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
185 Integer batteryLevel = newConfig.battery;
186 switch (channelUID.getId()) {
187 case CHANNEL_BATTERY_LEVEL:
188 if (batteryLevel != null) {
189 updateState(channelUID, new DecimalType(batteryLevel.longValue()));
192 case CHANNEL_BATTERY_LOW:
193 if (batteryLevel != null) {
194 updateState(channelUID, OnOffType.from(batteryLevel <= 10));
198 // other cases covered by sub-class
203 * Update channel value from {@link SensorState} object - override to include further channels
207 * @param initializing
209 protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
210 switch (channelUID.getId()) {
211 case CHANNEL_LAST_UPDATED:
212 String lastUpdated = newState.lastupdated;
213 if (lastUpdated != null && !"none".equals(lastUpdated)) {
214 updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated));
217 case CHANNEL_BATTERY_LOW:
218 Boolean lowBattery = newState.lowbattery;
219 if (lowBattery != null) {
220 updateState(channelUID, OnOffType.from(lowBattery));
224 // other cases covered by sub-class
229 public void messageReceived(String sensorID, DeconzBaseMessage message) {
230 logger.trace("{} received {}", thing.getUID(), message);
231 if (message instanceof SensorMessage) {
232 SensorMessage sensorMessage = (SensorMessage) message;
233 SensorConfig sensorConfig = sensorMessage.config;
234 if (sensorConfig != null) {
235 this.sensorConfig = sensorConfig;
236 updateChannels(sensorConfig);
238 SensorState sensorState = sensorMessage.state;
239 if (sensorState != null) {
240 updateChannels(sensorState, false);
245 private void updateChannels(SensorConfig newConfig) {
246 List<String> configChannels = getConfigChannels();
247 thing.getChannels().stream().map(Channel::getUID)
248 .filter(channelUID -> configChannels.contains(channelUID.getId()))
249 .forEach((channelUID) -> valueUpdated(channelUID, newConfig));
252 protected void updateChannels(SensorState newState, boolean initializing) {
253 sensorState = newState;
254 thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing));
257 protected void updateSwitchChannel(ChannelUID channelUID, @Nullable Boolean value) {
261 updateState(channelUID, OnOffType.from(value));
264 protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
265 updateState(channelUID, new StringType(value));
268 protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
272 updateState(channelUID, new DecimalType(value.longValue()));
275 protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit) {
276 updateQuantityTypeChannel(channelUID, value, unit, 1.0);
279 protected void updateQuantityTypeChannel(ChannelUID channelUID, @Nullable Number value, Unit<?> unit,
284 updateState(channelUID, new QuantityType<>(value.doubleValue() * scaling, unit));