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";
90 private static final String MODE_WEEKLY = "WEEKLY";
91 private static final String MODE_AUTO = "AUTO";
93 public OpenWebNetThermoregulationHandler(Thing thing) {
98 public void initialize() {
100 ThingTypeUID thingType = thing.getThingTypeUID();
101 isCentralUnit = OpenWebNetBindingConstants.THING_TYPE_BUS_THERMO_CU.equals(thingType);
102 if (!isCentralUnit) {
103 if (!((WhereThermo) deviceWhere).isProbe()) {
104 Object standAloneConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_STANDALONE);
105 if (standAloneConfig != null) {
106 isStandAlone = Boolean.parseBoolean(standAloneConfig.toString());
108 logger.debug("@@@@ THERMO ZONE INITIALIZE isStandAlone={}", isStandAlone);
111 // central unit must have WHERE=#0 or WHERE=0 or WHERE=#0#n
112 String w = deviceWhere.value();
113 if (w == null || !("0".equals(w) || "#0".equals(w) || w.startsWith("#0#"))) {
114 logger.warn("initialize() Invalid WHERE={} for Central Unit.", deviceWhere.value());
115 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
116 "@text/offline.conf-error-where");
123 protected void handleChannelCommand(ChannelUID channel, Command command) {
124 switch (channel.getId()) {
125 case CHANNEL_TEMP_SETPOINT:
126 handleSetpoint(command);
128 case CHANNEL_FUNCTION:
129 handleFunction(command);
134 case CHANNEL_FAN_SPEED:
135 handleSetFanSpeed(command);
137 case CHANNEL_CU_WEEKLY_PROGRAM_NUMBER:
138 case CHANNEL_CU_SCENARIO_PROGRAM_NUMBER:
139 handleSetProgramNumber(channel, command);
142 logger.warn("handleChannelCommand() Unsupported ChannelUID {}", channel.getId());
148 protected void requestChannelState(ChannelUID channel) {
149 super.requestChannelState(channel);
150 refreshDevice(false);
154 protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
155 return new WhereThermo(wStr);
159 protected String ownIdPrefix() {
160 return Who.THERMOREGULATION.value().toString();
163 private void handleSetFanSpeed(Command command) {
164 if (command instanceof StringType) {
165 Where w = deviceWhere;
168 Thermoregulation.FanCoilSpeed speed = Thermoregulation.FanCoilSpeed.valueOf(command.toString());
169 send(Thermoregulation.requestWriteFanCoilSpeed(w.value(), speed));
170 } catch (OWNException e) {
171 logger.warn("handleSetFanSpeed() {}", e.getMessage());
172 } catch (IllegalArgumentException e) {
173 logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command,
174 getThing().getUID());
179 logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command, getThing().getUID());
183 private void handleSetProgramNumber(ChannelUID channel, Command command) {
184 if (command instanceof DecimalType) {
185 if (!isCentralUnit) {
186 logger.warn("handleSetProgramNumber() This command can be sent only for a Central Unit.");
189 int programNumber = ((DecimalType) command).intValue();
190 boolean updateOpMode = false;
192 if (CHANNEL_CU_WEEKLY_PROGRAM_NUMBER.equals(channel.getId())) {
193 updateOpMode = currentMode.isWeekly();
194 currentWeeklyPrgNum = programNumber;
195 logger.debug("handleSetProgramNumber() currentWeeklyPrgNum changed to: {}", programNumber);
197 updateOpMode = currentMode.isScenario();
198 currentScenarioPrgNum = programNumber;
199 logger.debug("handleSetProgramNumber() currentScenarioPrgNum changed to: {}", programNumber);
202 // force OperationMode update if we are already in SCENARIO or WEEKLY mode
205 Thermoregulation.OperationMode newMode = Thermoregulation.OperationMode
206 .valueOf(currentMode.mode() + "_" + programNumber);
207 logger.debug("handleSetProgramNumber() new mode {}", newMode);
208 send(Thermoregulation.requestWriteMode(getWhere(deviceWhere.value()), newMode, currentFunction,
209 currentSetPointTemp));
210 } catch (OWNException e) {
211 logger.warn("handleSetProgramNumber() {}", e.getMessage());
212 } catch (IllegalArgumentException e) {
213 logger.warn("handleSetProgramNumber() Unsupported command {} for thing {}", command,
214 getThing().getUID());
216 } else { // just update channel
217 updateState(channel, new DecimalType(programNumber));
220 logger.warn("handleSetProgramNumber() Unsupported command {} for thing {}", command, getThing().getUID());
224 private void handleSetpoint(Command command) {
225 if (command instanceof QuantityType || command instanceof DecimalType) {
226 Where w = deviceWhere;
229 if (command instanceof QuantityType) {
230 QuantityType<?> tempCelsius = ((QuantityType<?>) command).toUnit(SIUnits.CELSIUS);
231 if (tempCelsius != null) {
232 newTemp = tempCelsius.doubleValue();
235 newTemp = ((DecimalType) command).doubleValue();
238 send(Thermoregulation.requestWriteSetpointTemperature(getWhere(w.value()), newTemp,
240 } catch (MalformedFrameException | OWNException e) {
241 logger.warn("handleSetpoint() {}", e.getMessage());
245 logger.warn("handleSetpoint() Unsupported command {} for thing {}", command, getThing().getUID());
249 private void handleMode(Command command) {
250 if (command instanceof StringType) {
251 Where w = deviceWhere;
254 Thermoregulation.OperationMode newMode = Thermoregulation.OperationMode.OFF;
256 if (isCentralUnit && WhatThermo.isComplex(command.toString())) {
257 int programNumber = 0;
258 if (MODE_WEEKLY.equalsIgnoreCase(command.toString())) {
259 programNumber = currentWeeklyPrgNum;
261 programNumber = currentScenarioPrgNum;
263 newMode = Thermoregulation.OperationMode.valueOf(command.toString() + "_" + programNumber);
264 currentMode = newMode;
266 if (MODE_AUTO.equalsIgnoreCase(command.toString())) {
267 newMode = Thermoregulation.OperationMode.PROGRAM;
269 newMode = Thermoregulation.OperationMode.valueOf(command.toString());
272 send(Thermoregulation.requestWriteMode(getWhere(w.value()), newMode, currentFunction,
273 currentSetPointTemp));
274 } catch (OWNException e) {
275 logger.warn("handleMode() {}", e.getMessage());
276 } catch (IllegalArgumentException e) {
277 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
282 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
286 private String getWhere(String where) {
288 if (where.charAt(0) == '#') {
290 } else { // to support old configurations for CU with where="0"
294 return isStandAlone ? where : "#" + where;
298 private void handleFunction(Command command) {
299 if (command instanceof StringType) {
300 Where w = deviceWhere;
303 Thermoregulation.Function function = Thermoregulation.Function.valueOf(command.toString());
304 send(Thermoregulation.requestWriteFunction(w.value(), function));
305 } catch (OWNException e) {
306 logger.warn("handleFunction() {}", e.getMessage());
307 } catch (IllegalArgumentException e) {
308 logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
313 logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
318 protected void handleMessage(BaseOpenMessage msg) {
319 super.handleMessage(msg);
320 logger.debug("@@@@ Thermo.handleMessage(): {}", msg.toStringVerbose());
321 Thermoregulation tmsg = (Thermoregulation) msg;
323 WhatThermo tWhat = (WhatThermo) msg.getWhat();
325 logger.debug("handleMessage() Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
328 if (tWhat.value() > 40) {
329 // it's a CU mode event, CU state events will follow shortly, so let's reset
334 case AT_LEAST_ONE_PROBE_ANTIFREEZE:
335 cuAtLeastOneProbeProtection = true;
337 case AT_LEAST_ONE_PROBE_MANUAL:
338 cuAtLeastOneProbeManual = true;
340 case AT_LEAST_ONE_PROBE_OFF:
341 cuAtLeastOneProbeOff = true;
344 cuBatteryStatus = CU_BATTERY_KO;
346 case FAILURE_DISCOVERED:
347 cuFailureDiscovered = true;
349 case RELEASE_SENSOR_LOCAL_ADJUST:
350 logger.debug("handleMessage(): Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
352 case REMOTE_CONTROL_DISABLED:
353 updateCURemoteControlStatus(CU_REMOTE_CONTROL_DISABLED);
355 case REMOTE_CONTROL_ENABLED:
356 updateCURemoteControlStatus(CU_REMOTE_CONTROL_ENABLED);
359 // check and update values of other channels (mode, function, temp)
360 updateModeAndFunction(tmsg);
361 updateSetpoint(tmsg);
367 if (tmsg.isCommand()) {
368 updateModeAndFunction(tmsg);
370 DimThermo dim = (DimThermo) tmsg.getDim();
373 case COMPLETE_PROBE_STATUS:
374 updateSetpoint(tmsg);
376 case PROBE_TEMPERATURE:
378 updateTemperature(tmsg);
380 case ACTUATOR_STATUS:
381 updateActuatorStatus(tmsg);
384 updateFanCoilSpeed(tmsg);
387 updateLocalOffset(tmsg);
390 updateValveStatus(tmsg);
393 logger.debug("handleMessage() Ignoring unsupported DIM {} for thing {}. Frame={}", tmsg.getDim(),
394 getThing().getUID(), tmsg);
400 private void updateModeAndFunction(Thermoregulation tmsg) {
401 if (tmsg.getWhat() == null) {
402 logger.warn("updateModeAndFunction() Could not parse Mode or Function from {} (WHAT is null)",
403 tmsg.getFrameValue());
406 Thermoregulation.WhatThermo w = Thermoregulation.WhatThermo.fromValue(tmsg.getWhat().value());
407 if (w.getMode() == null) {
408 logger.warn("updateModeAndFunction() Could not parse Mode from: {}", tmsg.getFrameValue());
411 if (w.getFunction() == null) {
412 logger.warn("updateModeAndFunction() Could not parse Function from: {}", tmsg.getFrameValue());
416 Thermoregulation.OperationMode operationMode = null;
417 if (w != WhatThermo.HEATING && w != WhatThermo.CONDITIONING) {
418 // *4*1*z## and *4*0*z## do not tell us which mode is the zone now
419 operationMode = w.getMode();
421 Thermoregulation.Function function = w.getFunction();
423 updateState(CHANNEL_FUNCTION, new StringType(function.toString()));
425 // must convert from OperationMode to Mode and set ProgramNumber when necessary
426 if (operationMode != null) {
428 if (operationMode == Thermoregulation.OperationMode.PROGRAM) { // translate PROGRAM -> AUTO
431 newMode = operationMode.mode();
433 updateState(CHANNEL_MODE, new StringType(newMode));
434 Integer programN = 0;
437 Integer prNum = operationMode.programNumber();
441 } catch (Exception e) {
442 logger.warn("updateModeAndFunction() Could not parse program number from: {}", tmsg.getFrameValue());
445 if (operationMode.isScenario()) {
446 logger.debug("{} - updateModeAndFunction() set SCENARIO program to: {}", getThing().getUID(), programN);
447 updateState(CHANNEL_CU_SCENARIO_PROGRAM_NUMBER, new DecimalType(programN));
448 currentScenarioPrgNum = programN;
450 if (operationMode.isWeekly()) {
451 logger.debug("{} - updateModeAndFunction() set WEEKLY program to: {}", getThing().getUID(), programN);
452 updateState(CHANNEL_CU_WEEKLY_PROGRAM_NUMBER, new DecimalType(programN));
453 currentWeeklyPrgNum = programN;
456 // store current function
457 currentFunction = function;
458 // in case of Central Unit store also current operation mode
460 currentMode = operationMode;
464 private void updateTemperature(Thermoregulation tmsg) {
466 double temp = Thermoregulation.parseTemperature(tmsg);
467 updateState(CHANNEL_TEMPERATURE, getAsQuantityTypeOrNull(temp, SIUnits.CELSIUS));
468 } catch (FrameException e) {
469 logger.warn("updateTemperature() FrameException on frame {}: {}", tmsg, e.getMessage());
470 updateState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
474 private void updateSetpoint(Thermoregulation tmsg) {
478 if (tmsg.getWhat() == null) {
479 logger.warn("updateSetpoint() Could not parse function from {} (what is null)",
480 tmsg.getFrameValue());
483 String[] parameters = tmsg.getWhatParams();
484 if (parameters.length > 0) {
485 // it should be like *4*WHAT#TTTT*#0##
486 newTemp = Thermoregulation.decodeTemperature(parameters[0]);
487 logger.debug("updateSetpoint() parsed temperature from {}: {} ---> {}", tmsg.toStringVerbose(),
488 parameters[0], newTemp);
491 newTemp = Thermoregulation.parseTemperature(tmsg);
494 updateState(CHANNEL_TEMP_SETPOINT, getAsQuantityTypeOrNull(newTemp, SIUnits.CELSIUS));
495 currentSetPointTemp = newTemp;
497 } catch (NumberFormatException e) {
498 logger.warn("updateSetpoint() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
499 updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
500 } catch (FrameException e) {
501 logger.warn("updateSetpoint() FrameException on frame {}: {}", tmsg, e.getMessage());
502 updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
506 private void updateFanCoilSpeed(Thermoregulation tmsg) {
508 Thermoregulation.FanCoilSpeed speed = Thermoregulation.parseFanCoilSpeed(tmsg);
509 updateState(CHANNEL_FAN_SPEED, new StringType(speed.toString()));
510 } catch (NumberFormatException e) {
511 logger.warn("updateFanCoilSpeed() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
512 updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
513 } catch (FrameException e) {
514 logger.warn("updateFanCoilSpeed() FrameException on frame {}: {}", tmsg, e.getMessage());
515 updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
519 private void updateValveStatus(Thermoregulation tmsg) {
521 Thermoregulation.ValveOrActuatorStatus cv = Thermoregulation.parseValveStatus(tmsg,
522 Thermoregulation.WhatThermo.CONDITIONING);
523 updateState(CHANNEL_CONDITIONING_VALVES, new StringType(cv.toString()));
525 Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseValveStatus(tmsg,
526 Thermoregulation.WhatThermo.HEATING);
527 updateState(CHANNEL_HEATING_VALVES, new StringType(hv.toString()));
528 } catch (FrameException e) {
529 logger.warn("updateValveStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
530 updateState(CHANNEL_CONDITIONING_VALVES, UnDefType.UNDEF);
531 updateState(CHANNEL_HEATING_VALVES, UnDefType.UNDEF);
535 private void updateActuatorStatus(Thermoregulation tmsg) {
537 Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseActuatorStatus(tmsg);
538 updateState(CHANNEL_ACTUATORS, new StringType(hv.toString()));
539 } catch (FrameException e) {
540 logger.warn("updateActuatorStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
541 updateState(CHANNEL_ACTUATORS, UnDefType.UNDEF);
545 private void updateLocalOffset(Thermoregulation tmsg) {
547 Thermoregulation.LocalOffset offset = Thermoregulation.parseLocalOffset(tmsg);
548 updateState(CHANNEL_LOCAL_OFFSET, new StringType(offset.toString()));
549 logger.debug("updateLocalOffset() {}: {}", tmsg, offset.toString());
551 } catch (FrameException e) {
552 logger.warn("updateLocalOffset() FrameException on frame {}: {}", tmsg, e.getMessage());
553 updateState(CHANNEL_LOCAL_OFFSET, UnDefType.UNDEF);
557 private void updateCURemoteControlStatus(String status) {
558 updateState(CHANNEL_CU_REMOTE_CONTROL, new StringType(status));
559 logger.debug("updateCURemoteControlStatus(): {}", status);
562 private void resetCUState() {
563 logger.debug("########### resetting CU state");
564 cuAtLeastOneProbeOff = false;
565 cuAtLeastOneProbeProtection = false;
566 cuAtLeastOneProbeManual = false;
567 cuBatteryStatus = CU_BATTERY_OK;
568 cuFailureDiscovered = false;
570 cuStateChannelsUpdateSchedule = scheduler.schedule(() -> {
571 updateCUStateChannels();
572 }, CU_STATE_CHANNELS_UPDATE_DELAY, TimeUnit.MILLISECONDS);
575 private void updateCUStateChannels() {
576 logger.debug("@@@@ updating CU state channels");
577 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_OFF, OnOffType.from(cuAtLeastOneProbeOff));
578 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_PROTECTION, OnOffType.from(cuAtLeastOneProbeProtection));
579 updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_MANUAL, OnOffType.from(cuAtLeastOneProbeManual));
580 updateState(CHANNEL_CU_BATTERY_STATUS, new StringType(cuBatteryStatus));
581 updateState(CHANNEL_CU_FAILURE_DISCOVERED, OnOffType.from(cuFailureDiscovered));
584 private Boolean channelExists(String channelID) {
585 return thing.getChannel(channelID) != null;
589 protected void refreshDevice(boolean refreshAll) {
590 logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
592 if (deviceWhere != null) {
593 String whereStr = deviceWhere.value();
597 send(Thermoregulation.requestStatus(getWhere(whereStr)));
598 } catch (OWNException e) {
599 logger.warn("refreshDevice() central unit returned OWNException {}", e.getMessage());
605 send(Thermoregulation.requestTemperature(whereStr));
607 if (!((WhereThermo) deviceWhere).isProbe()) {
608 // for bus_thermo_zone request also other single channels updates
609 send(Thermoregulation.requestSetPointTemperature(whereStr));
610 send(Thermoregulation.requestMode(whereStr));
612 // refresh ONLY subscribed channels
613 if (channelExists(CHANNEL_FAN_SPEED)) {
614 send(Thermoregulation.requestFanCoilSpeed(whereStr));
616 if (channelExists(CHANNEL_CONDITIONING_VALVES) || channelExists(CHANNEL_HEATING_VALVES)) {
617 send(Thermoregulation.requestValvesStatus(whereStr));
619 if (channelExists(CHANNEL_ACTUATORS)) {
620 send(Thermoregulation.requestActuatorsStatus(whereStr));
622 if (channelExists(CHANNEL_LOCAL_OFFSET)) {
623 send(Thermoregulation.requestLocalOffset(whereStr));
626 } catch (OWNException e) {
627 logger.warn("refreshDevice() where='{}' returned OWNException {}", whereStr, e.getMessage());
630 logger.debug("refreshDevice() where is null");
635 public void dispose() {
636 ScheduledFuture<?> s = cuStateChannelsUpdateSchedule;
639 logger.debug("dispose() - scheduler stopped.");