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.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
17 import static org.openhab.core.library.unit.Units.PERCENT;
19 import java.math.BigDecimal;
20 import java.util.List;
23 import javax.measure.quantity.Temperature;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.deconz.internal.dto.SensorConfig;
28 import org.openhab.binding.deconz.internal.dto.SensorState;
29 import org.openhab.binding.deconz.internal.dto.ThermostatUpdateConfig;
30 import org.openhab.binding.deconz.internal.types.ThermostatMode;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.OpenClosedType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.binding.builder.ThingBuilder;
40 import org.openhab.core.thing.type.ChannelKind;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import com.google.gson.Gson;
50 * This sensor Thermostat 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 * Only the Thermostat 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 Lukas Agethen - Initial contribution
64 public class SensorThermostatThingHandler extends SensorBaseThingHandler {
65 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_THERMOSTAT);
67 private static final List<String> CONFIG_CHANNELS = List.of(CHANNEL_EXTERNAL_WINDOW_OPEN, CHANNEL_BATTERY_LEVEL,
68 CHANNEL_BATTERY_LOW, CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE,
69 CHANNEL_THERMOSTAT_LOCKED);
71 private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class);
73 public SensorThermostatThingHandler(Thing thing, Gson gson) {
78 public void handleCommand(ChannelUID channelUID, Command command) {
79 if (command instanceof RefreshType) {
80 sensorState.buttonevent = null;
81 valueUpdated(channelUID, sensorState, false);
84 ThermostatUpdateConfig newConfig = new ThermostatUpdateConfig();
85 switch (channelUID.getId()) {
86 case CHANNEL_THERMOSTAT_LOCKED -> newConfig.locked = OnOffType.ON.equals(command);
87 case CHANNEL_HEATSETPOINT -> {
88 Integer newHeatsetpoint = getTemperatureFromCommand(command);
89 if (newHeatsetpoint == null) {
90 logger.warn("Heatsetpoint must not be null.");
93 newConfig.heatsetpoint = newHeatsetpoint;
95 case CHANNEL_TEMPERATURE_OFFSET -> {
96 Integer newOffset = getTemperatureFromCommand(command);
97 if (newOffset == null) {
98 logger.warn("Offset must not be null.");
101 newConfig.offset = newOffset;
103 case CHANNEL_THERMOSTAT_MODE -> {
104 if (command instanceof StringType stringCommand) {
105 String thermostatMode = stringCommand.toString();
107 newConfig.mode = ThermostatMode.valueOf(thermostatMode);
108 } catch (IllegalArgumentException ex) {
109 logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode,
110 ThermostatMode.values());
113 if (newConfig.mode == ThermostatMode.UNKNOWN) {
114 logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode,
115 ThermostatMode.values());
122 case CHANNEL_EXTERNAL_WINDOW_OPEN -> newConfig.externalwindowopen = OpenClosedType.OPEN.equals(command);
124 // no supported command
129 sendCommand(newConfig, command, channelUID, null);
133 protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
134 super.valueUpdated(channelUID, newConfig);
135 ThermostatMode thermostatMode = newConfig.mode;
136 String mode = thermostatMode != null ? thermostatMode.name() : ThermostatMode.UNKNOWN.name();
137 switch (channelUID.getId()) {
138 case CHANNEL_THERMOSTAT_LOCKED -> updateSwitchChannel(channelUID, newConfig.locked);
139 case CHANNEL_HEATSETPOINT ->
140 updateQuantityTypeChannel(channelUID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100);
141 case CHANNEL_TEMPERATURE_OFFSET ->
142 updateQuantityTypeChannel(channelUID, newConfig.offset, CELSIUS, 1.0 / 100);
143 case CHANNEL_THERMOSTAT_MODE -> updateState(channelUID, new StringType(mode));
144 case CHANNEL_EXTERNAL_WINDOW_OPEN -> {
145 Boolean open = newConfig.externalwindowopen;
147 updateState(channelUID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
154 protected void valueUpdated(ChannelUID channelUID, SensorState newState, boolean initializing) {
155 super.valueUpdated(channelUID, newState, initializing);
156 switch (channelUID.getId()) {
157 case CHANNEL_TEMPERATURE -> updateQuantityTypeChannel(channelUID, newState.temperature, CELSIUS, 1.0 / 100);
158 case CHANNEL_VALVE_POSITION -> {
159 Integer valve = newState.valve;
160 if (valve == null || valve < 0 || valve > 100) {
161 updateState(channelUID, UnDefType.UNDEF);
163 updateQuantityTypeChannel(channelUID, valve, PERCENT, 1.0);
166 case CHANNEL_WINDOW_OPEN -> {
167 String open = newState.windowopen;
169 updateState(channelUID, "Closed".equals(open) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
172 case CHANNEL_THERMOSTAT_ON -> updateSwitchChannel(channelUID, newState.on);
177 protected boolean createTypeSpecificChannels(ThingBuilder thingBuilder, SensorConfig sensorConfig,
178 SensorState sensorState) {
179 boolean thingEdited = false;
180 if (sensorConfig.locked != null && createChannel(thingBuilder, CHANNEL_THERMOSTAT_LOCKED, ChannelKind.STATE)) {
183 if (sensorState.valve != null && createChannel(thingBuilder, CHANNEL_VALVE_POSITION, ChannelKind.STATE)) {
186 if (sensorState.on != null && createChannel(thingBuilder, CHANNEL_THERMOSTAT_ON, ChannelKind.STATE)) {
189 if (sensorState.windowopen != null && createChannel(thingBuilder, CHANNEL_WINDOW_OPEN, ChannelKind.STATE)) {
192 if (sensorConfig.externalwindowopen != null
193 && createChannel(thingBuilder, CHANNEL_EXTERNAL_WINDOW_OPEN, ChannelKind.STATE)) {
201 protected List<String> getConfigChannels() {
202 return CONFIG_CHANNELS;
205 private @Nullable Integer getTemperatureFromCommand(Command command) {
206 BigDecimal newTemperature;
207 if (command instanceof DecimalType decimalCommand) {
208 newTemperature = decimalCommand.toBigDecimal();
209 } else if (command instanceof QuantityType) {
210 @SuppressWarnings("unchecked")
211 QuantityType<Temperature> temperatureCelsius = ((QuantityType<Temperature>) command).toUnit(CELSIUS);
212 if (temperatureCelsius != null) {
213 newTemperature = temperatureCelsius.toBigDecimal();
220 return newTemperature.scaleByPowerOfTen(2).intValue();