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;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.openhab.binding.deconz.internal.Util;
23 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
24 import org.openhab.binding.deconz.internal.dto.SensorConfig;
25 import org.openhab.binding.deconz.internal.dto.SensorMessage;
26 import org.openhab.binding.deconz.internal.dto.SensorState;
27 import org.openhab.binding.deconz.internal.types.ResourceType;
28 import org.openhab.core.thing.Channel;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.thing.binding.builder.ThingBuilder;
34 import org.openhab.core.thing.type.ChannelKind;
35 import org.openhab.core.types.Command;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import com.google.gson.Gson;
42 * This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
44 * It waits for the bridge to come online, grab the websocket connection and bridge configuration
45 * and registers to the websocket connection as a listener.
47 * A REST API call is made to get the initial sensor state.
49 * Every sensor and switch is supported by this Thing, because a unified state is kept
50 * in {@link #sensorState}. Every field that got received by the REST API for this specific
51 * sensor is published to the framework.
53 * @author David Graeff - Initial contribution
54 * @author Lukas Agethen - Refactored to provide better extensibility
57 public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
58 private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
60 * The sensor state. Contains all possible fields for all supported sensors and switches
62 protected SensorConfig sensorConfig = new SensorConfig();
63 protected SensorState sensorState = new SensorState();
65 * Prevent a dispose/init cycle while this flag is set. Use for property updates
67 private boolean ignoreConfigurationUpdate;
69 public SensorBaseThingHandler(Thing thing, Gson gson) {
70 super(thing, gson, ResourceType.SENSORS);
74 public abstract void handleCommand(ChannelUID channelUID, Command command);
76 protected abstract boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorState,
77 SensorState sensorConfig);
79 protected abstract List<String> getConfigChannels();
82 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
83 if (!ignoreConfigurationUpdate) {
84 super.handleConfigurationUpdate(configurationParameters);
89 protected void processStateResponse(DeconzBaseMessage stateResponse) {
90 if (!(stateResponse instanceof SensorMessage sensorMessage)) {
94 sensorConfig = Objects.requireNonNullElse(sensorMessage.config, new SensorConfig());
95 sensorState = Objects.requireNonNullElse(sensorMessage.state, new SensorState());
97 // Add some information about the sensor
98 if (!sensorConfig.reachable) {
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
103 Map<String, String> editProperties = editProperties();
104 editProperties.put(UNIQUE_ID, sensorMessage.uniqueid);
105 editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, sensorMessage.swversion);
106 editProperties.put(Thing.PROPERTY_VENDOR, sensorMessage.manufacturername);
107 editProperties.put(Thing.PROPERTY_MODEL_ID, sensorMessage.modelid);
109 ignoreConfigurationUpdate = true;
111 updateProperties(editProperties);
113 // Some sensors support optional channels
114 // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
115 // any battery-powered sensor
116 ThingBuilder thingBuilder = editThing();
117 boolean thingEdited = false;
119 if (sensorConfig.battery != null) {
120 if (createChannel(thingBuilder, CHANNEL_BATTERY_LEVEL, ChannelKind.STATE)) {
123 if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) {
126 } else if (sensorState.lowbattery != null) {
127 // if sensorConfig.battery != null the channel is already added
128 if (createChannel(thingBuilder, CHANNEL_BATTERY_LOW, ChannelKind.STATE)) {
133 if (createTypeSpecificChannels(thingBuilder, sensorConfig, sensorState)) {
137 if (checkLastSeen(thingBuilder, sensorMessage.lastseen)) {
141 // if the thing was edited, we update it now
143 logger.debug("Thing configuration changed, updating thing.");
144 updateThing(thingBuilder.build());
146 ignoreConfigurationUpdate = false;
149 updateChannels(sensorConfig);
150 updateChannels(sensorState, true);
152 updateStatus(ThingStatus.ONLINE);
156 * Update channel value from {@link SensorConfig} object - override to include further channels
161 protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
162 Integer batteryLevel = newConfig.battery;
163 if (batteryLevel != null) {
164 switch (channelUID.getId()) {
165 case CHANNEL_BATTERY_LEVEL -> updateDecimalTypeChannel(channelUID, batteryLevel.longValue());
166 case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, batteryLevel <= 10);
167 // other cases covered by subclass
173 * Update channel value from {@link SensorState} object - override to include further channels
177 * @param initializing
179 protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
180 switch (channelUID.getId()) {
181 case CHANNEL_LAST_UPDATED -> {
182 String lastUpdated = newState.lastupdated;
183 if (lastUpdated != null && !"none".equals(lastUpdated)) {
184 updateState(channelUID, Util.convertTimestampToDateTime(lastUpdated));
187 case CHANNEL_BATTERY_LOW -> updateSwitchChannel(channelUID, newState.lowbattery);
188 // other cases covered by subclass
193 public void messageReceived(DeconzBaseMessage message) {
194 logger.trace("{} received {}", thing.getUID(), message);
195 if (message instanceof SensorMessage sensorMessage) {
196 SensorConfig sensorConfig = sensorMessage.config;
197 if (sensorConfig != null) {
198 if (sensorConfig.reachable) {
199 updateStatus(ThingStatus.ONLINE);
200 updateChannels(sensorConfig);
202 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
205 SensorState sensorState = sensorMessage.state;
206 if (sensorState != null) {
207 updateChannels(sensorState, false);
212 private void updateChannels(SensorConfig newConfig) {
213 this.sensorConfig = newConfig;
214 List<String> configChannels = getConfigChannels();
215 thing.getChannels().stream().map(Channel::getUID)
216 .filter(channelUID -> configChannels.contains(channelUID.getId()))
217 .forEach((channelUID) -> valueUpdated(channelUID, newConfig));
220 protected void updateChannels(SensorState newState, boolean initializing) {
221 sensorState = newState;
222 thing.getChannels().forEach(channel -> valueUpdated(channel.getUID(), newState, initializing));