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.openwebnet.internal.handler;
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
24 import org.openhab.core.library.types.DecimalType;
25 import org.openhab.core.library.types.OnOffType;
26 import org.openhab.core.library.types.QuantityType;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.library.unit.SIUnits;
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.ThingTypeUID;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.UnDefType;
36 import org.openwebnet4j.communication.OWNException;
37 import org.openwebnet4j.message.BaseOpenMessage;
38 import org.openwebnet4j.message.FrameException;
39 import org.openwebnet4j.message.MalformedFrameException;
40 import org.openwebnet4j.message.Thermoregulation;
41 import org.openwebnet4j.message.Thermoregulation.DimThermo;
42 import org.openwebnet4j.message.Thermoregulation.WhatThermo;
43 import org.openwebnet4j.message.Where;
44 import org.openwebnet4j.message.WhereThermo;
45 import org.openwebnet4j.message.Who;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link OpenWebNetThermoregulationHandler} is responsible for handling
51 * commands/messages for Thermoregulation Things. It extends the abstract
52 * {@link OpenWebNetThingHandler}.
54 * @author Massimo Valla - Initial contribution. Added support for 4-zones CU.
55 * Rafactoring and fixed CU state channels updates.
56 * @author Gilberto Cocchi - Initial contribution.
57 * @author Andrea Conte - Added support for 99-zone CU and CU state channels.
60 public class OpenWebNetThermoregulationHandler extends OpenWebNetThingHandler {
62 private final Logger logger = LoggerFactory.getLogger(OpenWebNetThermoregulationHandler.class);
64 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.THERMOREGULATION_SUPPORTED_THING_TYPES;
66 private double currentSetPointTemp = 20.0d;
68 private Thermoregulation.@Nullable Function currentFunction = null;
69 private Thermoregulation.@Nullable OperationMode currentMode = null;
70 private int currentWeeklyPrgNum = 1;
71 private int currentScenarioPrgNum = 1;
73 private boolean isStandAlone = true; // true if zone is not associated to a CU
74 private boolean isCentralUnit = false;
76 private boolean cuAtLeastOneProbeOff = false;
77 private boolean cuAtLeastOneProbeProtection = false;
78 private boolean cuAtLeastOneProbeManual = false;
79 private String cuBatteryStatus = CU_BATTERY_OK;
80 private boolean cuFailureDiscovered = false;
82 private @Nullable ScheduledFuture<?> cuStateChannelsUpdateSchedule;
84 public static final int CU_STATE_CHANNELS_UPDATE_DELAY = 1500; // msec
86 private static final String CU_REMOTE_CONTROL_ENABLED = "ENABLED";
87 private static final String CU_REMOTE_CONTROL_DISABLED = "DISABLED";
88 private static final String CU_BATTERY_OK = "OK";
89 private static final String CU_BATTERY_KO = "KO";
91 public OpenWebNetThermoregulationHandler(Thing thing) {
96 public void initialize() {
98 ThingTypeUID thingType = thing.getThingTypeUID();
99 isCentralUnit = OpenWebNetBindingConstants.THING_TYPE_BUS_THERMO_CU.equals(thingType);
100 if (!isCentralUnit) {
101 if (!((WhereThermo) deviceWhere).isProbe()) {
102 Object standAloneConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_STANDALONE);
103 if (standAloneConfig != null) {
104 isStandAlone = Boolean.parseBoolean(standAloneConfig.toString());
106 logger.debug("@@@@ THERMO ZONE INITIALIZE isStandAlone={}", isStandAlone);
109 // central unit must have WHERE=#0 or WHERE=0 or WHERE=#0#n
110 String w = deviceWhere.value();
111 if (w == null || !("0".equals(w) || "#0".equals(w) || w.startsWith("#0#"))) {
112 logger.warn("initialize() Invalid WHERE={} for Central Unit.", deviceWhere.value());
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114 "@text/offline.conf-error-where");
121 protected void handleChannelCommand(ChannelUID channel, Command command) {
122 switch (channel.getId()) {
123 case CHANNEL_TEMP_SETPOINT:
124 handleSetpoint(command);
126 case CHANNEL_FUNCTION:
127 handleFunction(command);
132 case CHANNEL_FAN_SPEED:
133 handleSetFanSpeed(command);
135 case CHANNEL_CU_WEEKLY_PROGRAM_NUMBER:
136 case CHANNEL_CU_SCENARIO_PROGRAM_NUMBER:
137 handleSetProgramNumber(channel, command);
140 logger.warn("handleChannelCommand() Unsupported ChannelUID {}", channel.getId());
146 protected void requestChannelState(ChannelUID channel) {
147 super.requestChannelState(channel);
148 refreshDevice(false);
152 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
153 return new WhereThermo(wStr);
157 protected String ownIdPrefix() {
158 return Who.THERMOREGULATION.value().toString();
161 private void handleSetFanSpeed(Command command) {
162 if (command instanceof StringType) {
163 Where w = deviceWhere;
166 Thermoregulation.FanCoilSpeed speed = Thermoregulation.FanCoilSpeed.valueOf(command.toString());
167 send(Thermoregulation.requestWriteFanCoilSpeed(w.value(), speed));
168 } catch (OWNException e) {
169 logger.warn("handleSetFanSpeed() {}", e.getMessage());
170 } catch (IllegalArgumentException e) {
171 logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command,
172 getThing().getUID());
177 logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command, getThing().getUID());
181 private void handleSetProgramNumber(ChannelUID channel, Command command) {
182 if (command instanceof DecimalType) {
183 if (!isCentralUnit) {
184 logger.warn("handleSetProgramNumber() This command can be sent only for a Central Unit.");
187 int programNumber = ((DecimalType) command).intValue();
188 boolean updateOpMode = false;
190 if (CHANNEL_CU_WEEKLY_PROGRAM_NUMBER.equals(channel.getId())) {
191 updateOpMode = currentMode.isWeekly();
192 currentWeeklyPrgNum = programNumber;
193 logger.debug("handleSetProgramNumber() currentWeeklyPrgNum changed to: {}", programNumber);
195 updateOpMode = currentMode.isScenario();
196 currentScenarioPrgNum = programNumber;
197 logger.debug("handleSetProgramNumber() currentScenarioPrgNum changed to: {}", programNumber);
200 // force OperationMode update if we are already in SCENARIO or WEEKLY mode
203 Thermoregulation.OperationMode newMode = Thermoregulation.OperationMode
204 .valueOf(currentMode.mode() + "_" + programNumber);
205 logger.debug("handleSetProgramNumber() new mode {}", newMode);
206 send(Thermoregulation.requestWriteMode(getWhere(deviceWhere.value()), newMode, currentFunction,
207 currentSetPointTemp));
208 } catch (OWNException e) {
209 logger.warn("handleSetProgramNumber() {}", e.getMessage());
210 } catch (IllegalArgumentException e) {
211 logger.warn("handleSetProgramNumber() Unsupported command {} for thing {}", command,
212 getThing().getUID());
214 } else { // just update channel
215 updateState(channel, new DecimalType(programNumber));
218 logger.warn("handleSetProgramNumber() Unsupported command {} for thing {}", command, getThing().getUID());
222 private void handleSetpoint(Command command) {
223 if (command instanceof QuantityType || command instanceof DecimalType) {
224 Where w = deviceWhere;
227 if (command instanceof QuantityType) {
228 QuantityType<?> tempCelsius = ((QuantityType<?>) command).toUnit(SIUnits.CELSIUS);
229 if (tempCelsius != null) {
230 newTemp = tempCelsius.doubleValue();
233 newTemp = ((DecimalType) command).doubleValue();
236 send(Thermoregulation.requestWriteSetpointTemperature(getWhere(w.value()), newTemp,
238 } catch (MalformedFrameException | OWNException e) {
239 logger.warn("handleSetpoint() {}", e.getMessage());
243 logger.warn("handleSetpoint() Unsupported command {} for thing {}", command, getThing().getUID());
247 private void handleMode(Command command) {
248 if (command instanceof StringType) {
249 Where w = deviceWhere;
252 Thermoregulation.OperationMode newMode = Thermoregulation.OperationMode.OFF;
254 if (isCentralUnit && WhatThermo.isComplex(command.toString())) {
255 int programNumber = 0;
256 if ("WEEKLY".equalsIgnoreCase(command.toString())) {
257 programNumber = currentWeeklyPrgNum;
259 programNumber = currentScenarioPrgNum;
261 newMode = Thermoregulation.OperationMode.valueOf(command.toString() + "_" + programNumber);
262 currentMode = newMode;
264 newMode = Thermoregulation.OperationMode.valueOf(command.toString());
266 send(Thermoregulation.requestWriteMode(getWhere(w.value()), newMode, currentFunction,
267 currentSetPointTemp));
268 } catch (OWNException e) {
269 logger.warn("handleMode() {}", e.getMessage());
270 } catch (IllegalArgumentException e) {
271 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
276 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
280 private String getWhere(String where) {
282 if (where.charAt(0) == '#') {
284 } else { // to support old configurations for CU with where="0"
288 return isStandAlone ? where : "#" + where;
292 private void handleFunction(Command command) {
293 if (command instanceof StringType) {
294 Where w = deviceWhere;
297 Thermoregulation.Function function = Thermoregulation.Function.valueOf(command.toString());
298 send(Thermoregulation.requestWriteFunction(w.value(), function));
299 } catch (OWNException e) {
300 logger.warn("handleFunction() {}", e.getMessage());
301 } catch (IllegalArgumentException e) {
302 logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
307 logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
312 protected void handleMessage(BaseOpenMessage msg) {
313 super.handleMessage(msg);
314 logger.debug("@@@@ Thermo.handleMessage(): {}", msg.toStringVerbose());
315 Thermoregulation tmsg = (Thermoregulation) msg;
317 WhatThermo tWhat = (WhatThermo) msg.getWhat();
319 logger.debug("handleMessage() Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
322 if (tWhat.value() > 40) {
323 // it's a CU mode event, CU state events will follow shortly, so let's reset
328 case AT_LEAST_ONE_PROBE_ANTIFREEZE:
329 cuAtLeastOneProbeProtection = true;
331 case AT_LEAST_ONE_PROBE_MANUAL:
332 cuAtLeastOneProbeManual = true;
334 case AT_LEAST_ONE_PROBE_OFF:
335 cuAtLeastOneProbeOff = true;
338 cuBatteryStatus = CU_BATTERY_KO;
340 case FAILURE_DISCOVERED:
341 cuFailureDiscovered = true;
343 case RELEASE_SENSOR_LOCAL_ADJUST:
344 logger.debug("handleMessage(): Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
346 case REMOTE_CONTROL_DISABLED:
347 updateCURemoteControlStatus(CU_REMOTE_CONTROL_DISABLED);
349 case REMOTE_CONTROL_ENABLED:
350 updateCURemoteControlStatus(CU_REMOTE_CONTROL_ENABLED);
353 // check and update values of other channels (mode, function, temp)
354 updateModeAndFunction(tmsg);
355 updateSetpoint(tmsg);
361 if (tmsg.isCommand()) {
362 updateModeAndFunction(tmsg);
364 DimThermo dim = (DimThermo) tmsg.getDim();
367 case COMPLETE_PROBE_STATUS:
368 updateSetpoint(tmsg);
370 case PROBE_TEMPERATURE:
372 updateTemperature(tmsg);
374 case ACTUATOR_STATUS:
375 updateActuatorStatus(tmsg);
378 updateFanCoilSpeed(tmsg);
381 updateLocalOffset(tmsg);
384 updateValveStatus(tmsg);
387 logger.debug("handleMessage() Ignoring unsupported DIM {} for thing {}. Frame={}", tmsg.getDim(),
388 getThing().getUID(), tmsg);
394 private void updateModeAndFunction(Thermoregulation tmsg) {
395 if (tmsg.getWhat() == null) {
396 logger.warn("updateModeAndFunction() Could not parse Mode or Function from {} (WHAT is null)",
397 tmsg.getFrameValue());
400 Thermoregulation.WhatThermo w = Thermoregulation.WhatThermo.fromValue(tmsg.getWhat().value());
401 if (w.getMode() == null) {
402 logger.warn("updateModeAndFunction() Could not parse Mode from: {}", tmsg.getFrameValue());
405 if (w.getFunction() == null) {
406 logger.warn("updateModeAndFunction() Could not parse Function from: {}", tmsg.getFrameValue());
410 Thermoregulation.OperationMode operationMode = null;
411 if (w != WhatThermo.HEATING && w != WhatThermo.CONDITIONING) {
412 // *4*1*z## and *4*0*z## do not tell us which mode is the zone now
413 operationMode = w.getMode();
415 Thermoregulation.Function function = w.getFunction();
417 updateState(CHANNEL_FUNCTION, new StringType(function.toString()));
419 // must convert from OperationMode to Mode and set ProgramNumber when necessary
420 if (operationMode != null) {
421 updateState(CHANNEL_MODE, new StringType(operationMode.mode()));
422 Integer programN = 0;
425 Integer prNum = operationMode.programNumber();
429 } catch (Exception e) {
430 logger.warn("updateModeAndFunction() Could not parse program number from: {}", tmsg.getFrameValue());
433 if (operationMode.isScenario()) {
434 logger.debug("{} - updateModeAndFunction() set SCENARIO program to: {}", getThing().getUID(), programN);
435 updateState(CHANNEL_CU_SCENARIO_PROGRAM_NUMBER, new DecimalType(programN));
436 currentScenarioPrgNum = programN;
438 if (operationMode.isWeekly()) {
439 logger.debug("{} - updateModeAndFunction() set WEEKLY program to: {}", getThing().getUID(), programN);
440 updateState(CHANNEL_CU_WEEKLY_PROGRAM_NUMBER, new DecimalType(programN));
441 currentWeeklyPrgNum = programN;
444 // store current function
445 currentFunction = function;
446 // in case of Central Unit store also current operation mode
448 currentMode = operationMode;
452 private void updateTemperature(Thermoregulation tmsg) {
454 double temp = Thermoregulation.parseTemperature(tmsg);
455 updateState(CHANNEL_TEMPERATURE, getAsQuantityTypeOrNull(temp, SIUnits.CELSIUS));
456 } catch (FrameException e) {
457 logger.warn("updateTemperature() FrameException on frame {}: {}", tmsg, e.getMessage());
458 updateState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
462 private void updateSetpoint(Thermoregulation tmsg) {
466 if (tmsg.getWhat() == null) {
467 logger.warn("updateSetpoint() Could not parse function from {} (what is null)",
468 tmsg.getFrameValue());
471 String[] parameters = tmsg.getWhatParams();
472 if (parameters.length > 0) {
473 // it should be like *4*WHAT#TTTT*#0##
474 newTemp = Thermoregulation.decodeTemperature(parameters[0]);
475 logger.debug("updateSetpoint() parsed temperature from {}: {} ---> {}", tmsg.toStringVerbose(),
476 parameters[0], newTemp);
479 newTemp = Thermoregulation.parseTemperature(tmsg);
482 updateState(CHANNEL_TEMP_SETPOINT, getAsQuantityTypeOrNull(newTemp, SIUnits.CELSIUS));
483 currentSetPointTemp = newTemp;
485 } catch (NumberFormatException e) {
486 logger.warn("updateSetpoint() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
487 updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
488 } catch (FrameException e) {
489 logger.warn("updateSetpoint() FrameException on frame {}: {}", tmsg, e.getMessage());
490 updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
494 private void updateFanCoilSpeed(Thermoregulation tmsg) {
496 Thermoregulation.FanCoilSpeed speed = Thermoregulation.parseFanCoilSpeed(tmsg);
497 updateState(CHANNEL_FAN_SPEED, new StringType(speed.toString()));
498 } catch (NumberFormatException e) {
499 logger.warn("updateFanCoilSpeed() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
500 updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
501 } catch (FrameException e) {
502 logger.warn("updateFanCoilSpeed() FrameException on frame {}: {}", tmsg, e.getMessage());
503 updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
507 private void updateValveStatus(Thermoregulation tmsg) {
509 Thermoregulation.ValveOrActuatorStatus cv = Thermoregulation.parseValveStatus(tmsg,
510 Thermoregulation.WhatThermo.CONDITIONING);
511 updateState(CHANNEL_CONDITIONING_VALVES, new StringType(cv.toString()));
513 Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseValveStatus(tmsg,
514 Thermoregulation.WhatThermo.HEATING);
515 updateState(CHANNEL_HEATING_VALVES, new StringType(hv.toString()));
516 } catch (FrameException e) {
517 logger.warn("updateValveStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
518 updateState(CHANNEL_CONDITIONING_VALVES, UnDefType.UNDEF);
519 updateState(CHANNEL_HEATING_VALVES, UnDefType.UNDEF);
523 private void updateActuatorStatus(Thermoregulation tmsg) {
525 Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseActuatorStatus(tmsg);
526 updateState(CHANNEL_ACTUATORS, new StringType(hv.toString()));
527 } catch (FrameException e) {
528 logger.warn("updateActuatorStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
529 updateState(CHANNEL_ACTUATORS, UnDefType.UNDEF);
533 private void updateLocalOffset(Thermoregulation tmsg) {
535 Thermoregulation.LocalOffset offset = Thermoregulation.parseLocalOffset(tmsg);
536 updateState(CHANNEL_LOCAL_OFFSET, new StringType(offset.toString()));
537 logger.debug("updateLocalOffset() {}: {}", tmsg, offset.toString());
539 } catch (FrameException e) {
540 logger.warn("updateLocalOffset() FrameException on frame {}: {}", tmsg, e.getMessage());
541 updateState(CHANNEL_LOCAL_OFFSET, UnDefType.UNDEF);
545 private void updateCURemoteControlStatus(String status) {
546 updateState(CHANNEL_CU_REMOTE_CONTROL, new StringType(status));
547 logger.debug("updateCURemoteControlStatus(): {}", status);
550 private void resetCUState() {
551 logger.debug("########### resetting CU state");
552 cuAtLeastOneProbeOff = false;
553 cuAtLeastOneProbeProtection = false;
554 cuAtLeastOneProbeManual = false;
555 cuBatteryStatus = CU_BATTERY_OK;
556 cuFailureDiscovered = false;
558 cuStateChannelsUpdateSchedule = scheduler.schedule(() -> {
559 updateCUStateChannels();
560 }, CU_STATE_CHANNELS_UPDATE_DELAY, TimeUnit.MILLISECONDS);
563 private void updateCUStateChannels() {
564 logger.debug("@@@@ updating CU state channels");
565 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_OFF, OnOffType.from(cuAtLeastOneProbeOff));
566 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_PROTECTION, OnOffType.from(cuAtLeastOneProbeProtection));
567 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_MANUAL, OnOffType.from(cuAtLeastOneProbeManual));
568 updateState(CHANNEL_CU_BATTERY_STATUS, new StringType(cuBatteryStatus));
569 updateState(CHANNEL_CU_FAILURE_DISCOVERED, OnOffType.from(cuFailureDiscovered));
572 private Boolean channelExists(String channelID) {
573 return thing.getChannel(channelID) != null;
577 protected void refreshDevice(boolean refreshAll) {
578 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
580 if (deviceWhere != null) {
581 String whereStr = deviceWhere.value();
585 send(Thermoregulation.requestStatus(getWhere(whereStr)));
586 } catch (OWNException e) {
587 logger.warn("refreshDevice() central unit returned OWNException {}", e.getMessage());
593 send(Thermoregulation.requestTemperature(whereStr));
595 if (!((WhereThermo) deviceWhere).isProbe()) {
596 // for bus_thermo_zone request also other single channels updates
597 send(Thermoregulation.requestSetPointTemperature(whereStr));
598 send(Thermoregulation.requestMode(whereStr));
600 // refresh ONLY subscribed channels
601 if (channelExists(CHANNEL_FAN_SPEED)) {
602 send(Thermoregulation.requestFanCoilSpeed(whereStr));
604 if (channelExists(CHANNEL_CONDITIONING_VALVES) || channelExists(CHANNEL_HEATING_VALVES)) {
605 send(Thermoregulation.requestValvesStatus(whereStr));
607 if (channelExists(CHANNEL_ACTUATORS)) {
608 send(Thermoregulation.requestActuatorsStatus(whereStr));
610 if (channelExists(CHANNEL_LOCAL_OFFSET)) {
611 send(Thermoregulation.requestLocalOffset(whereStr));
614 } catch (OWNException e) {
615 logger.warn("refreshDevice() where='{}' returned OWNException {}", whereStr, e.getMessage());
618 logger.debug("refreshDevice() where is null");
623 public void dispose() {
624 ScheduledFuture<?> s = cuStateChannelsUpdateSchedule;
627 logger.debug("dispose() - scheduler stopped.");