2 * Copyright (c) 2010-2024 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.velbus.internal.handler;
15 import static org.openhab.binding.velbus.internal.VelbusBindingConstants.*;
17 import org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.openhab.binding.velbus.internal.packets.VelbusPacket;
19 import org.openhab.binding.velbus.internal.packets.VelbusSensorSettingsRequestPacket;
20 import org.openhab.binding.velbus.internal.packets.VelbusSetTemperaturePacket;
21 import org.openhab.binding.velbus.internal.packets.VelbusThermostatModePacket;
22 import org.openhab.binding.velbus.internal.packets.VelbusThermostatOperatingModePacket;
23 import org.openhab.core.library.types.DecimalType;
24 import org.openhab.core.library.types.QuantityType;
25 import org.openhab.core.library.types.StringType;
26 import org.openhab.core.library.unit.SIUnits;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.core.thing.CommonTriggerEvents;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.RefreshType;
36 * The {@link VelbusThermostatHandler} is responsible for handling commands, which are
37 * sent to one of the channels.
39 * @author Cedric Boon - Initial contribution
42 public abstract class VelbusThermostatHandler extends VelbusTemperatureSensorHandler {
43 private static final double THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION = 0.5;
45 private static final StringType OPERATING_MODE_HEATING = new StringType("HEATING");
46 private static final StringType OPERATING_MODE_COOLING = new StringType("COOLING");
48 private static final byte OPERATING_MODE_MASK = (byte) 0x80;
49 private static final byte COOLING_MODE_MASK = (byte) 0x80;
51 private static final StringType MODE_COMFORT = new StringType("COMFORT");
52 private static final StringType MODE_DAY = new StringType("DAY");
53 private static final StringType MODE_NIGHT = new StringType("NIGHT");
54 private static final StringType MODE_SAFE = new StringType("SAFE");
56 private static final byte MODE_MASK = (byte) 0x70;
57 private static final byte COMFORT_MODE_MASK = (byte) 0x40;
58 private static final byte DAY_MODE_MASK = (byte) 0x20;
59 private static final byte NIGHT_MODE_MASK = (byte) 0x10;
61 private final ChannelUID currentTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
62 "currentTemperatureSetpoint");
63 private final ChannelUID heatingModeComfortTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
64 "heatingModeComfortTemperatureSetpoint");
65 private final ChannelUID heatingModeDayTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
66 "heatingModeDayTemperatureSetpoint");
67 private final ChannelUID heatingModeNightTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
68 "heatingModeNightTemperatureSetpoint");
69 private final ChannelUID heatingModeAntifrostTemperatureSetpointChannel = new ChannelUID(thing.getUID(),
70 "thermostat", "heatingModeAntiFrostTemperatureSetpoint");
71 private final ChannelUID coolingModeComfortTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
72 "coolingModeComfortTemperatureSetpoint");
73 private final ChannelUID coolingModeDayTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
74 "coolingModeDayTemperatureSetpoint");
75 private final ChannelUID coolingModeNightTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
76 "coolingModeNightTemperatureSetpoint");
77 private final ChannelUID coolingModeSafeTemperatureSetpointChannel = new ChannelUID(thing.getUID(), "thermostat",
78 "coolingModeSafeTemperatureSetpoint");
79 private final ChannelUID operatingModeChannel = new ChannelUID(thing.getUID(), "thermostat", "operatingMode");
80 private final ChannelUID modeChannel = new ChannelUID(thing.getUID(), "thermostat", "mode");
81 private final ChannelUID heaterChannel = new ChannelUID(thing.getUID(), "thermostat", "heater");
82 private final ChannelUID boostChannel = new ChannelUID(thing.getUID(), "thermostat", "boost");
83 private final ChannelUID pumpChannel = new ChannelUID(thing.getUID(), "thermostat", "pump");
84 private final ChannelUID coolerChannel = new ChannelUID(thing.getUID(), "thermostat", "cooler");
85 private final ChannelUID alarm1Channel = new ChannelUID(thing.getUID(), "thermostat", "alarm1");
86 private final ChannelUID alarm2Channel = new ChannelUID(thing.getUID(), "thermostat", "alarm2");
87 private final ChannelUID alarm3Channel = new ChannelUID(thing.getUID(), "thermostat", "alarm3");
88 private final ChannelUID alarm4Channel = new ChannelUID(thing.getUID(), "thermostat", "alarm4");
90 public VelbusThermostatHandler(Thing thing, int numberOfSubAddresses, ChannelUID temperatureChannel) {
91 super(thing, numberOfSubAddresses, temperatureChannel);
95 public void handleCommand(ChannelUID channelUID, Command command) {
96 super.handleCommand(channelUID, command);
98 VelbusBridgeHandler velbusBridgeHandler = getVelbusBridgeHandler();
99 if (velbusBridgeHandler == null) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
104 if (isThermostatChannel(channelUID) && command instanceof RefreshType) {
105 VelbusSensorSettingsRequestPacket packet = new VelbusSensorSettingsRequestPacket(
106 getModuleAddress().getAddress());
108 byte[] packetBytes = packet.getBytes();
109 velbusBridgeHandler.sendPacket(packetBytes);
110 } else if (isThermostatChannel(channelUID)
111 && (command instanceof QuantityType<?> || command instanceof DecimalType)) {
112 byte temperatureVariable = determineTemperatureVariable(channelUID);
113 QuantityType<?> temperatureInDegreesCelcius = (command instanceof QuantityType<?> qt)
114 ? qt.toUnit(SIUnits.CELSIUS)
115 : new QuantityType<>(((DecimalType) command), SIUnits.CELSIUS);
117 if (temperatureInDegreesCelcius != null) {
118 byte temperature = convertToTwoComplementByte(temperatureInDegreesCelcius.doubleValue(),
119 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
121 VelbusSetTemperaturePacket packet = new VelbusSetTemperaturePacket(getModuleAddress().getAddress(),
122 temperatureVariable, temperature);
124 byte[] packetBytes = packet.getBytes();
125 velbusBridgeHandler.sendPacket(packetBytes);
127 } else if (channelUID.equals(operatingModeChannel) && command instanceof StringType stringCommand) {
128 byte commandByte = stringCommand.equals(OPERATING_MODE_HEATING) ? COMMAND_SET_HEATING_MODE
129 : COMMAND_SET_COOLING_MODE;
131 VelbusThermostatOperatingModePacket packet = new VelbusThermostatOperatingModePacket(
132 getModuleAddress().getAddress(), commandByte);
134 byte[] packetBytes = packet.getBytes();
135 velbusBridgeHandler.sendPacket(packetBytes);
136 } else if (channelUID.equals(modeChannel) && command instanceof StringType stringCommand) {
137 byte commandByte = COMMAND_SWITCH_TO_SAFE_MODE;
138 if (stringCommand.equals(MODE_COMFORT)) {
139 commandByte = COMMAND_SWITCH_TO_COMFORT_MODE;
140 } else if (stringCommand.equals(MODE_DAY)) {
141 commandByte = COMMAND_SWITCH_TO_DAY_MODE;
142 } else if (stringCommand.equals(MODE_NIGHT)) {
143 commandByte = COMMAND_SWITCH_TO_NIGHT_MODE;
146 VelbusThermostatModePacket packet = new VelbusThermostatModePacket(getModuleAddress().getAddress(),
149 byte[] packetBytes = packet.getBytes();
150 velbusBridgeHandler.sendPacket(packetBytes);
152 logger.debug("The command '{}' is not supported by this handler.", command.getClass());
157 public void onPacketReceived(byte[] packet) {
158 super.onPacketReceived(packet);
160 logger.trace("onPacketReceived() was called");
162 if (packet[0] == VelbusPacket.STX && packet.length >= 5) {
163 byte address = packet[2];
164 byte command = packet[4];
166 if (command == COMMAND_TEMP_SENSOR_SETTINGS_PART1 && packet.length >= 9) {
167 byte currentTemperatureSetByte = packet[5];
168 byte heatingModeComfortTemperatureSetByte = packet[6];
169 byte heatingModeDayTemperatureSetByte = packet[7];
170 byte heatingModeNightTemperatureSetByte = packet[8];
171 byte heatingModeAntiFrostTemperatureSetByte = packet[9];
173 double currentTemperatureSet = convertFromTwoComplementByte(currentTemperatureSetByte,
174 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
175 double heatingModeComfortTemperatureSet = convertFromTwoComplementByte(
176 heatingModeComfortTemperatureSetByte, THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
177 double heatingModeDayTemperatureSet = convertFromTwoComplementByte(heatingModeDayTemperatureSetByte,
178 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
179 double heatingModeNightTemperatureSet = convertFromTwoComplementByte(heatingModeNightTemperatureSetByte,
180 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
181 double heatingModeAntiFrostTemperatureSet = convertFromTwoComplementByte(
182 heatingModeAntiFrostTemperatureSetByte, THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
184 updateState(currentTemperatureSetpointChannel,
185 new QuantityType<>(currentTemperatureSet, SIUnits.CELSIUS));
186 updateState(heatingModeComfortTemperatureSetpointChannel,
187 new QuantityType<>(heatingModeComfortTemperatureSet, SIUnits.CELSIUS));
188 updateState(heatingModeDayTemperatureSetpointChannel,
189 new QuantityType<>(heatingModeDayTemperatureSet, SIUnits.CELSIUS));
190 updateState(heatingModeNightTemperatureSetpointChannel,
191 new QuantityType<>(heatingModeNightTemperatureSet, SIUnits.CELSIUS));
192 updateState(heatingModeAntifrostTemperatureSetpointChannel,
193 new QuantityType<>(heatingModeAntiFrostTemperatureSet, SIUnits.CELSIUS));
194 } else if (command == COMMAND_TEMP_SENSOR_SETTINGS_PART2 && packet.length >= 8) {
195 byte coolingModeComfortTemperatureSetByte = packet[5];
196 byte coolingModeDayTemperatureSetByte = packet[6];
197 byte coolingModeNightTemperatureSetByte = packet[7];
198 byte coolingModeSafeTemperatureSetByte = packet[8];
200 double coolingModeComfortTemperatureSet = convertFromTwoComplementByte(
201 coolingModeComfortTemperatureSetByte, THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
202 double coolingModeDayTemperatureSet = convertFromTwoComplementByte(coolingModeDayTemperatureSetByte,
203 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
204 double coolingModeNightTemperatureSet = convertFromTwoComplementByte(coolingModeNightTemperatureSetByte,
205 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
206 double coolingModeSafeTemperatureSet = convertFromTwoComplementByte(coolingModeSafeTemperatureSetByte,
207 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
209 updateState(coolingModeComfortTemperatureSetpointChannel,
210 new QuantityType<>(coolingModeComfortTemperatureSet, SIUnits.CELSIUS));
211 updateState(coolingModeDayTemperatureSetpointChannel,
212 new QuantityType<>(coolingModeDayTemperatureSet, SIUnits.CELSIUS));
213 updateState(coolingModeNightTemperatureSetpointChannel,
214 new QuantityType<>(coolingModeNightTemperatureSet, SIUnits.CELSIUS));
215 updateState(coolingModeSafeTemperatureSetpointChannel,
216 new QuantityType<>(coolingModeSafeTemperatureSet, SIUnits.CELSIUS));
217 } else if (command == COMMAND_TEMP_SENSOR_STATUS && packet.length >= 9) {
218 byte operatingMode = packet[5];
219 byte targetTemperature = packet[9];
221 if ((operatingMode & OPERATING_MODE_MASK) == COOLING_MODE_MASK) {
222 updateState(operatingModeChannel, OPERATING_MODE_COOLING);
224 updateState(operatingModeChannel, OPERATING_MODE_HEATING);
227 if ((operatingMode & MODE_MASK) == COMFORT_MODE_MASK) {
228 updateState(modeChannel, MODE_COMFORT);
229 } else if ((operatingMode & MODE_MASK) == DAY_MODE_MASK) {
230 updateState(modeChannel, MODE_DAY);
231 } else if ((operatingMode & MODE_MASK) == NIGHT_MODE_MASK) {
232 updateState(modeChannel, MODE_NIGHT);
234 updateState(modeChannel, MODE_SAFE);
237 double targetTemperatureValue = convertFromTwoComplementByte(targetTemperature,
238 THERMOSTAT_TEMPERATURE_SETPOINT_RESOLUTION);
239 updateState(currentTemperatureSetpointChannel,
240 new QuantityType<>(targetTemperatureValue, SIUnits.CELSIUS));
241 } else if (address != this.getModuleAddress().getAddress() && command == COMMAND_PUSH_BUTTON_STATUS) {
242 byte outputChannelsJustActivated = packet[5];
243 byte outputChannelsJustDeactivated = packet[6];
245 triggerThermostatChannels(outputChannelsJustActivated, CommonTriggerEvents.PRESSED);
246 triggerThermostatChannels(outputChannelsJustDeactivated, CommonTriggerEvents.RELEASED);
251 private void triggerThermostatChannels(byte outputChannels, String event) {
252 if ((outputChannels & 0x01) == 0x01) {
253 triggerChannel(heaterChannel, event);
255 if ((outputChannels & 0x02) == 0x02) {
256 triggerChannel(boostChannel, event);
258 if ((outputChannels & 0x04) == 0x04) {
259 triggerChannel(pumpChannel, event);
261 if ((outputChannels & 0x08) == 0x08) {
262 triggerChannel(coolerChannel, event);
264 if ((outputChannels & 0x10) == 0x10) {
265 triggerChannel(alarm1Channel, event);
267 if ((outputChannels & 0x20) == 0x20) {
268 triggerChannel(alarm2Channel, event);
270 if ((outputChannels & 0x40) == 0x40) {
271 triggerChannel(alarm3Channel, event);
273 if ((outputChannels & 0x80) == 0x80) {
274 triggerChannel(alarm4Channel, event);
278 protected boolean isThermostatChannel(ChannelUID channelUID) {
279 return channelUID.equals(currentTemperatureSetpointChannel)
280 || channelUID.equals(heatingModeComfortTemperatureSetpointChannel)
281 || channelUID.equals(heatingModeDayTemperatureSetpointChannel)
282 || channelUID.equals(heatingModeNightTemperatureSetpointChannel)
283 || channelUID.equals(heatingModeAntifrostTemperatureSetpointChannel)
284 || channelUID.equals(coolingModeComfortTemperatureSetpointChannel)
285 || channelUID.equals(coolingModeDayTemperatureSetpointChannel)
286 || channelUID.equals(coolingModeNightTemperatureSetpointChannel)
287 || channelUID.equals(coolingModeSafeTemperatureSetpointChannel)
288 || channelUID.equals(operatingModeChannel) || channelUID.equals(modeChannel);
291 protected byte determineTemperatureVariable(ChannelUID channelUID) {
292 if (channelUID.equals(currentTemperatureSetpointChannel)) {
294 } else if (channelUID.equals(heatingModeComfortTemperatureSetpointChannel)) {
296 } else if (channelUID.equals(heatingModeDayTemperatureSetpointChannel)) {
298 } else if (channelUID.equals(heatingModeNightTemperatureSetpointChannel)) {
300 } else if (channelUID.equals(heatingModeAntifrostTemperatureSetpointChannel)) {
302 } else if (channelUID.equals(coolingModeComfortTemperatureSetpointChannel)) {
304 } else if (channelUID.equals(coolingModeDayTemperatureSetpointChannel)) {
306 } else if (channelUID.equals(coolingModeNightTemperatureSetpointChannel)) {
308 } else if (channelUID.equals(coolingModeSafeTemperatureSetpointChannel)) {
311 throw new IllegalArgumentException("The given channelUID is not a thermostat channel: " + channelUID);
315 protected double convertFromTwoComplementByte(byte value, double resolution) {
316 return ((value & 0x80) == 0x00) ? value * resolution : ((value & 0x7F) - 0x80) * resolution;
319 protected byte convertToTwoComplementByte(double value, double resolution) {
320 return (byte) (value / resolution);