]> git.basschouten.com Git - openhab-addons.git/blob
9809c08b77ef061c6bd5910ccd45621b737159b7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.openwebnet.internal.handler;
14
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
16
17 import java.util.Set;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
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.communication.Response;
38 import org.openwebnet4j.message.BaseOpenMessage;
39 import org.openwebnet4j.message.FrameException;
40 import org.openwebnet4j.message.MalformedFrameException;
41 import org.openwebnet4j.message.Thermoregulation;
42 import org.openwebnet4j.message.Thermoregulation.DimThermo;
43 import org.openwebnet4j.message.Thermoregulation.Function;
44 import org.openwebnet4j.message.Thermoregulation.OperationMode;
45 import org.openwebnet4j.message.Thermoregulation.WhatThermo;
46 import org.openwebnet4j.message.Thermoregulation.WhatThermoType;
47 import org.openwebnet4j.message.Where;
48 import org.openwebnet4j.message.WhereThermo;
49 import org.openwebnet4j.message.Who;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link OpenWebNetThermoregulationHandler} is responsible for handling
55  * commands/messages for Thermoregulation Things. It extends the abstract
56  * {@link OpenWebNetThingHandler}.
57  *
58  * @author Massimo Valla - Initial contribution. Added support for 4-zones CU.
59  *         Rafactoring and fixed CU state channels updates. Completed support
60  *         for HOLIDAY/VACATION modes and refactored mode handling.
61  * @author Gilberto Cocchi - Initial contribution.
62  * @author Andrea Conte - Added support for 99-zone CU and CU state channels.
63  */
64 @NonNullByDefault
65 public class OpenWebNetThermoregulationHandler extends OpenWebNetThingHandler {
66
67     private final Logger logger = LoggerFactory.getLogger(OpenWebNetThermoregulationHandler.class);
68
69     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.THERMOREGULATION_SUPPORTED_THING_TYPES;
70
71     private double currentSetPointTemp = 20.0d;
72
73     private Thermoregulation.@Nullable Function currentFunction = null;
74     private Thermoregulation.@Nullable OperationMode currentMode = null;
75     private int currentWeeklyPrgNum = 1;
76     private int currentScenarioPrgNum = 1;
77     private int currentVacationDays = 1;
78
79     private boolean isStandAlone = true; // true if zone is not associated to a CU
80     private boolean isCentralUnit = false;
81
82     private boolean cuAtLeastOneProbeOff = false;
83     private boolean cuAtLeastOneProbeProtection = false;
84     private boolean cuAtLeastOneProbeManual = false;
85     private String cuBatteryStatus = CU_BATTERY_OK;
86     private boolean cuFailureDiscovered = false;
87
88     private @Nullable ScheduledFuture<?> cuStateChannelsUpdateSchedule;
89
90     public static final int CU_STATE_CHANNELS_UPDATE_DELAY = 1500; // msec
91
92     private static final String CU_REMOTE_CONTROL_ENABLED = "ENABLED";
93     private static final String CU_REMOTE_CONTROL_DISABLED = "DISABLED";
94     private static final String CU_BATTERY_OK = "OK";
95     private static final String CU_BATTERY_KO = "KO";
96     private static final String MODE_WEEKLY = "WEEKLY";
97
98     public OpenWebNetThermoregulationHandler(Thing thing) {
99         super(thing);
100     }
101
102     @Override
103     public void initialize() {
104         super.initialize();
105         ThingTypeUID thingType = thing.getThingTypeUID();
106         isCentralUnit = OpenWebNetBindingConstants.THING_TYPE_BUS_THERMO_CU.equals(thingType);
107         if (!isCentralUnit) {
108             if (!((WhereThermo) deviceWhere).isProbe()) {
109                 Object standAloneConfig = getConfig().get(OpenWebNetBindingConstants.CONFIG_PROPERTY_STANDALONE);
110                 if (standAloneConfig != null) {
111                     isStandAlone = Boolean.parseBoolean(standAloneConfig.toString());
112                 }
113                 logger.debug("@@@@  THERMO ZONE INITIALIZE isStandAlone={}", isStandAlone);
114             }
115         } else {
116             // central unit must have WHERE=#0 or WHERE=0 or WHERE=#0#n
117             String w = deviceWhere.value();
118             if (w == null || !("0".equals(w) || "#0".equals(w) || w.startsWith("#0#"))) {
119                 logger.warn("initialize() Invalid WHERE={} for Central Unit.", deviceWhere.value());
120                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
121                         "@text/offline.conf-error-where");
122                 return;
123             }
124         }
125     }
126
127     @Override
128     protected void handleChannelCommand(ChannelUID channel, Command command) {
129         switch (channel.getId()) {
130             case CHANNEL_TEMP_SETPOINT:
131                 handleSetpoint(command);
132                 break;
133             case CHANNEL_FUNCTION:
134                 handleFunction(command);
135                 break;
136             case CHANNEL_MODE:
137                 handleMode(command);
138                 break;
139             case CHANNEL_FAN_SPEED:
140                 handleSetFanSpeed(command);
141                 break;
142             case CHANNEL_CU_WEEKLY_PROGRAM_NUMBER:
143             case CHANNEL_CU_SCENARIO_PROGRAM_NUMBER:
144                 handleSetProgramNumber(channel, command);
145                 break;
146             case CHANNEL_CU_VACATION_DAYS:
147                 handleVacationDays(channel, command);
148                 break;
149             default: {
150                 logger.warn("handleChannelCommand() Unsupported ChannelUID {}", channel.getId());
151             }
152         }
153     }
154
155     @Override
156     protected void requestChannelState(ChannelUID channel) {
157         super.requestChannelState(channel);
158         refreshDevice(false);
159     }
160
161     @Override
162     protected Where buildBusWhere(String wStr) throws IllegalArgumentException {
163         return new WhereThermo(wStr);
164     }
165
166     @Override
167     protected String ownIdPrefix() {
168         return Who.THERMOREGULATION.value().toString();
169     }
170
171     private void handleSetFanSpeed(Command command) {
172         if (command instanceof StringType) {
173             Where w = deviceWhere;
174             if (w == null) {
175                 logger.warn("handleSetFanSpeed() Where is null for thing {}", getThing().getUID());
176                 return;
177             }
178             try {
179                 Thermoregulation.FanCoilSpeed speed = Thermoregulation.FanCoilSpeed.valueOf(command.toString());
180                 send(Thermoregulation.requestWriteFanCoilSpeed(w.value(), speed));
181             } catch (OWNException e) {
182                 logger.warn("handleSetFanSpeed() {}", e.getMessage());
183             } catch (IllegalArgumentException e) {
184                 logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command, getThing().getUID());
185                 return;
186             }
187         } else {
188             logger.warn("handleSetFanSpeed() Unsupported command {} for thing {}", command, getThing().getUID());
189         }
190     }
191
192     private void handleSetProgramNumber(ChannelUID channel, Command command) {
193         if (command instanceof DecimalType) {
194             if (!isCentralUnit) {
195                 logger.warn("handleSetProgramNumber() This command can be sent only for a Central Unit.");
196                 return;
197             }
198             Where w = deviceWhere;
199             if (w == null) {
200                 logger.warn("handleSetProgramNumber() Where is null for thing {}", getThing().getUID());
201                 return;
202             }
203
204             int programNumber = ((DecimalType) command).intValue();
205             boolean updateOpMode = false;
206
207             if (CHANNEL_CU_WEEKLY_PROGRAM_NUMBER.equals(channel.getId())) {
208                 if (programNumber < 1 || programNumber > 3) {
209                     logger.warn("handleSetProgramNumber() Invalid program number {} for thing {}", command,
210                             getThing().getUID());
211                     return;
212                 }
213                 updateOpMode = (currentMode == Thermoregulation.OperationMode.WEEKLY);
214                 currentWeeklyPrgNum = programNumber;
215                 logger.debug("handleSetProgramNumber() currentWeeklyPrgNum changed to: {}", programNumber);
216             } else {
217                 if (programNumber < 1 || programNumber > 16) {
218                     logger.warn("handleSetProgramNumber() Invalid program number {} for thing {}", command,
219                             getThing().getUID());
220                     return;
221                 }
222                 updateOpMode = (currentMode == Thermoregulation.OperationMode.SCENARIO);
223                 currentScenarioPrgNum = programNumber;
224                 logger.debug("handleSetProgramNumber() currentScenarioPrgNum changed to: {}", programNumber);
225             }
226
227             // force OperationMode update if we are already in SCENARIO or WEEKLY mode
228             if (updateOpMode) {
229                 try {
230                     send(Thermoregulation.requestWriteWeeklyScenarioMode(getWhere(w.value()), currentMode,
231                             currentFunction, programNumber));
232                 } catch (OWNException e) {
233                     logger.warn("handleSetProgramNumber() {}", e.getMessage());
234                 }
235             } else { // just update channel
236                 updateState(channel, new DecimalType(programNumber));
237             }
238         } else {
239             logger.warn("handleSetProgramNumber() Unsupported command {} for thing {}", command, getThing().getUID());
240         }
241     }
242
243     private void handleVacationDays(ChannelUID channel, Command command) {
244         if (command instanceof DecimalType) {
245             Where w = deviceWhere;
246             if (w == null) {
247                 logger.warn("handleVacationDays() Where is null for thing {}", getThing().getUID());
248                 return;
249             }
250             if (!isCentralUnit) {
251                 logger.warn("handleVacationDays() This command can be sent only for a Central Unit.");
252                 return;
253             }
254             int vacationDays = ((DecimalType) command).intValue();
255
256             if (vacationDays < 1 || vacationDays > 255) {
257                 logger.warn("handleVacationDays() vacation days must be between 1 and 255");
258                 return;
259             }
260
261             currentVacationDays = vacationDays;
262             logger.debug("handleVacationDays() currentVacationDays changed to: {}", currentVacationDays);
263
264             // force OperationMode update if we are already in VACATION mode
265             if (currentMode == Thermoregulation.OperationMode.VACATION) {
266                 try {
267                     send(Thermoregulation.requestWriteVacationMode(getWhere(w.value()), currentFunction,
268                             currentVacationDays, currentWeeklyPrgNum));
269                 } catch (OWNException e) {
270                     logger.warn("handleVacationDays() {}", e.getMessage());
271                 } catch (IllegalArgumentException e) {
272                     logger.warn("handleVacationDays() Unsupported command {} for thing {}", command,
273                             getThing().getUID());
274                 }
275             } else { // just update channel
276                 updateState(channel, new DecimalType(currentVacationDays));
277             }
278         } else {
279             logger.warn("handleVacationDays() Unsupported command {} for thing {}", command, getThing().getUID());
280         }
281     }
282
283     private void handleSetpoint(Command command) {
284         if (command instanceof QuantityType || command instanceof DecimalType) {
285             Where w = deviceWhere;
286             if (w == null) {
287                 logger.warn("handleSetpoint() Where is null for thing {}", getThing().getUID());
288                 return;
289             }
290             double newTemp = 0;
291             if (command instanceof QuantityType) {
292                 QuantityType<?> tempCelsius = ((QuantityType<?>) command).toUnit(SIUnits.CELSIUS);
293                 if (tempCelsius != null) {
294                     newTemp = tempCelsius.doubleValue();
295                 }
296             } else {
297                 newTemp = ((DecimalType) command).doubleValue();
298             }
299             if (newTemp >= 5.0 && newTemp <= 40.0) {
300                 try {
301                     Response res;
302                     res = send(Thermoregulation.requestWriteSetpointTemperature(getWhere(w.value()), newTemp,
303                             currentFunction));
304                     // For zones no setPoint temperature confirmation message is returned from gw,
305                     // so we update channel with current requested temp on successful response.
306                     if (!isCentralUnit && res != null && res.isSuccess()) {
307                         updateState(CHANNEL_TEMP_SETPOINT, getAsQuantityTypeOrNull(newTemp, SIUnits.CELSIUS));
308                         currentSetPointTemp = newTemp;
309                     }
310                 } catch (MalformedFrameException | OWNException e) {
311                     logger.warn("handleSetpoint() {}", e.getMessage());
312
313                 }
314             } else {
315                 logger.info("handleSetpoint() Setpoint temperature must be between 5°C and 40°C for thing {}",
316                         getThing().getUID());
317             }
318         } else {
319             logger.warn("handleSetpoint() Unsupported command {} for thing {}", command, getThing().getUID());
320         }
321     }
322
323     private void handleMode(Command command) {
324         if (command instanceof StringType) {
325             Where w = deviceWhere;
326             if (w == null) {
327                 logger.warn("handleMode() Where is null for thing {}", getThing().getUID());
328                 return;
329             }
330
331             String whStr = getWhere(w.value());
332             String cmdStr = command.toString().toUpperCase();
333             OperationMode newMode;
334             try {
335                 newMode = OperationMode.valueOf(cmdStr);
336             } catch (IllegalArgumentException e) {
337                 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
338                 return;
339             }
340             Thermoregulation tMsg = null;
341             if (isCentralUnit) { // CU accepted modes: MANUAL, OFF, PROTECTION, WEEKLY, SCENARIO, HOLIDAY,
342                                  // VACATION
343                 switch (newMode) {
344                     case MANUAL:
345                     case OFF:
346                     case PROTECTION:
347                         tMsg = Thermoregulation.requestWriteMode(whStr, newMode, currentFunction, currentSetPointTemp);
348                         break;
349                     case WEEKLY:
350                     case SCENARIO:
351                         int programNumber = (MODE_WEEKLY.equals(cmdStr) ? currentWeeklyPrgNum : currentScenarioPrgNum);
352                         tMsg = Thermoregulation.requestWriteWeeklyScenarioMode(whStr, newMode, currentFunction,
353                                 programNumber);
354                         break;
355                     case HOLIDAY:
356                         tMsg = Thermoregulation.requestWriteHolidayMode(whStr, currentFunction, currentWeeklyPrgNum);
357                         break;
358                     case VACATION:
359                         tMsg = Thermoregulation.requestWriteVacationMode(whStr, currentFunction, currentVacationDays,
360                                 currentWeeklyPrgNum);
361                         break;
362                     default:
363                         logger.warn("handleMode() Unsupported command {} for CU thing {}", command,
364                                 getThing().getUID());
365                 }
366             } else {// Zone accepted modes: MANUAL, OFF, PROTECTION, AUTO
367                 switch (newMode) {
368                     case MANUAL:
369                         tMsg = Thermoregulation.requestWriteMode(whStr, newMode, currentFunction, currentSetPointTemp);
370                         break;
371                     case OFF:
372                     case PROTECTION:
373                     case AUTO:
374                         tMsg = Thermoregulation.requestWriteMode(whStr, newMode, Function.GENERIC, currentSetPointTemp);
375                         break;
376                     default:
377                         logger.warn("handleMode() Unsupported command {} for ZONE thing {}", command,
378                                 getThing().getUID());
379                 }
380             }
381
382             if (tMsg != null) {
383                 try {
384                     send(tMsg);
385                 } catch (OWNException e) {
386                     logger.warn("handleMode() {}", e.getMessage());
387                 }
388             } else {
389                 logger.warn("handleMode() Unsupported command {} for thing {}", command, getThing().getUID());
390             }
391         }
392     }
393
394     private String getWhere(String where) {
395         if (isCentralUnit) {
396             if (where.charAt(0) == '#') {
397                 return where;
398             } else { // to support old configurations for CU with where="0"
399                 return "#" + where;
400             }
401         } else {
402             return isStandAlone ? where : "#" + where;
403         }
404     }
405
406     private void handleFunction(Command command) {
407         if (command instanceof StringType) {
408             Where w = deviceWhere;
409             if (w == null) {
410                 logger.warn("handleFunction() Where is null for thing {}", getThing().getUID());
411                 return;
412             }
413             try {
414                 Thermoregulation.Function function = Thermoregulation.Function.valueOf(command.toString());
415                 send(Thermoregulation.requestWriteFunction(w.value(), function));
416             } catch (OWNException e) {
417                 logger.warn("handleFunction() {}", e.getMessage());
418             } catch (IllegalArgumentException e) {
419                 logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
420                 return;
421             }
422         } else {
423             logger.warn("handleFunction() Unsupported command {} for thing {}", command, getThing().getUID());
424         }
425     }
426
427     @Override
428     protected void handleMessage(BaseOpenMessage msg) {
429         super.handleMessage(msg);
430         logger.debug("@@@@ Thermo.handleMessage(): {}", msg.toStringVerbose());
431         Thermoregulation tmsg = (Thermoregulation) msg;
432         if (isCentralUnit) {
433             WhatThermo tWhat = (WhatThermo) msg.getWhat();
434             if (tWhat == null) {
435                 logger.debug("handleMessage() Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
436                 return;
437             }
438             if (tWhat.value() > 40) {
439                 // it's a CU mode event, CU state events will follow shortly, so let's reset
440                 // their values
441                 resetCUState();
442             }
443             switch (tWhat.getType()) {
444                 case AT_LEAST_ONE_PROBE_ANTIFREEZE:
445                     cuAtLeastOneProbeProtection = true;
446                     break;
447                 case AT_LEAST_ONE_PROBE_MANUAL:
448                     cuAtLeastOneProbeManual = true;
449                     break;
450                 case AT_LEAST_ONE_PROBE_OFF:
451                     cuAtLeastOneProbeOff = true;
452                     break;
453                 case BATTERY_KO:
454                     cuBatteryStatus = CU_BATTERY_KO;
455                     break;
456                 case FAILURE_DISCOVERED:
457                     cuFailureDiscovered = true;
458                     break;
459                 case RELEASE_SENSOR_LOCAL_ADJUST:
460                     logger.debug("handleMessage(): Ignoring unsupported WHAT {}. Frame={}", tWhat, msg);
461                     break;
462                 case REMOTE_CONTROL_DISABLED:
463                     updateCURemoteControlStatus(CU_REMOTE_CONTROL_DISABLED);
464                     break;
465                 case REMOTE_CONTROL_ENABLED:
466                     updateCURemoteControlStatus(CU_REMOTE_CONTROL_ENABLED);
467                     break;
468                 default:
469                     // check and update values of other channels (mode, function, temp)
470                     updateModeAndFunction(tmsg);
471                     updateSetpoint(tmsg);
472                     break;
473             }
474             return;
475         }
476
477         if (tmsg.isCommand()) {
478             updateModeAndFunction(tmsg);
479         } else {
480             DimThermo dim = (DimThermo) tmsg.getDim();
481             switch (dim) {
482                 case TEMP_SETPOINT:
483                     updateSetpoint(tmsg);
484                     break;
485                 case COMPLETE_PROBE_STATUS:
486                     updateTargetTemperature(tmsg);
487                     break;
488                 case PROBE_TEMPERATURE:
489                 case TEMPERATURE:
490                     updateTemperature(tmsg);
491                     break;
492                 case ACTUATOR_STATUS:
493                     updateActuatorStatus(tmsg);
494                     break;
495                 case FAN_COIL_SPEED:
496                     updateFanCoilSpeed(tmsg);
497                     break;
498                 case OFFSET:
499                     updateLocalOffset(tmsg);
500                     break;
501                 case VALVES_STATUS:
502                     updateValveStatus(tmsg);
503                     break;
504                 default:
505                     logger.debug("handleMessage() Ignoring unsupported DIM {} for thing {}. Frame={}", tmsg.getDim(),
506                             getThing().getUID(), tmsg);
507                     break;
508             }
509         }
510     }
511
512     private void updateModeAndFunction(Thermoregulation tmsg) {
513         if (tmsg.getWhat() == null) {
514             logger.warn("updateModeAndFunction() Could not parse What {} (WHAT is null)", tmsg.getFrameValue());
515             return;
516         }
517         Thermoregulation.WhatThermo what = tmsg.new WhatThermo(tmsg.getWhat().value());
518
519         if (what.getFunction() == null) {
520             logger.warn("updateModeAndFunction() Could not parse Function from: {}", tmsg.getFrameValue());
521             return;
522         }
523         // update Function if it's not GENERIC
524         Thermoregulation.Function function = what.getFunction();
525         if (function != Function.GENERIC) {
526             if (currentFunction != function) {
527                 updateState(CHANNEL_FUNCTION, new StringType(function.toString()));
528                 currentFunction = function;
529             }
530         }
531         if (what.getType() == WhatThermoType.HEATING || what.getType() == WhatThermoType.CONDITIONING
532                 || what.getType() == WhatThermoType.GENERIC) {
533             // *4*1*z## and *4*0*z## do not tell us which mode is the zone now, so let's
534             // skip
535             return;
536         }
537
538         // update Mode
539         Thermoregulation.OperationMode operationMode = null;
540
541         operationMode = what.getMode();
542
543         if (operationMode != null) {
544             // set ProgramNumber/vacationDays channels when necessary
545             switch (operationMode) {
546                 case VACATION:
547                     updateVacationDays(tmsg);
548                     break;
549                 case WEEKLY:
550                 case SCENARIO:
551                     updateProgramNumber(tmsg);
552                     break;
553                 default:
554                     break;
555             }
556             if (!isCentralUnit && !(operationMode == OperationMode.AUTO || operationMode == OperationMode.OFF
557                     || operationMode == OperationMode.PROTECTION || operationMode == OperationMode.MANUAL)) {
558                 logger.warn("updateModeAndFunction() Unsupported mode for zone {} from message: {}",
559                         getThing().getUID(), tmsg.getFrameValue());
560                 return;
561             } else {
562                 if (currentMode != operationMode) {
563                     updateState(CHANNEL_MODE, new StringType(operationMode.name()));
564                     currentMode = operationMode;
565                 }
566             }
567         } else {
568             logger.warn("updateModeAndFunction() Unrecognized mode from message: {}", tmsg.getFrameValue());
569         }
570     }
571
572     private void updateVacationDays(Thermoregulation tmsg) {
573         @Nullable
574         Integer vDays = ((WhatThermo) tmsg.getWhat()).vacationDays();
575         if (vDays != null) {
576             logger.debug("{} - updateVacationDays() set VACATION DAYS to: {}", getThing().getUID(), vDays);
577             updateState(CHANNEL_CU_VACATION_DAYS, new DecimalType(vDays));
578             currentVacationDays = vDays;
579         }
580     }
581
582     private void updateProgramNumber(Thermoregulation tmsg) {
583         @Nullable
584         WhatThermo wt = (WhatThermo) tmsg.getWhat();
585         if (wt == null) {
586             return;
587         }
588         Integer prNum = wt.programNumber();
589         if (prNum != null) {
590             if (wt.getMode() == OperationMode.SCENARIO) {
591                 logger.debug("{} - updateProgramNumber() set SCENARIO program to: {}", getThing().getUID(), prNum);
592                 updateState(CHANNEL_CU_SCENARIO_PROGRAM_NUMBER, new DecimalType(prNum));
593                 currentScenarioPrgNum = prNum;
594             } else if (wt.getMode() == OperationMode.WEEKLY) {
595                 logger.debug("{} - updateProgramNumber() set WEEKLY program to: {}", getThing().getUID(), prNum);
596                 updateState(CHANNEL_CU_WEEKLY_PROGRAM_NUMBER, new DecimalType(prNum));
597                 currentWeeklyPrgNum = prNum;
598             }
599         }
600     }
601
602     private void updateTemperature(Thermoregulation tmsg) {
603         try {
604             double temp = Thermoregulation.parseTemperature(tmsg);
605             updateState(CHANNEL_TEMPERATURE, getAsQuantityTypeOrNull(temp, SIUnits.CELSIUS));
606         } catch (FrameException e) {
607             logger.warn("updateTemperature() FrameException on frame {}: {}", tmsg, e.getMessage());
608             updateState(CHANNEL_TEMPERATURE, UnDefType.UNDEF);
609         }
610     }
611
612     private void updateTargetTemperature(Thermoregulation tmsg) {
613         try {
614             double temp = Thermoregulation.parseTemperature(tmsg);
615             updateState(CHANNEL_TEMP_TARGET, getAsQuantityTypeOrNull(temp, SIUnits.CELSIUS));
616         } catch (FrameException e) {
617             logger.warn("updateTargetTemperature() FrameException on frame {}: {}", tmsg, e.getMessage());
618             updateState(CHANNEL_TEMP_TARGET, UnDefType.UNDEF);
619         }
620     }
621
622     private void updateSetpoint(Thermoregulation tmsg) {
623         try {
624             double newTemp = -1;
625             if (isCentralUnit) {
626                 WhatThermo tw = (WhatThermo) tmsg.getWhat();
627                 if (tw == null) {
628                     logger.warn("updateSetpoint() Could not parse what from {} (what is null)", tmsg.getFrameValue());
629                     return;
630                 }
631                 String[] parameters = tmsg.getWhatParams();
632                 if (parameters.length > 0 && tw.getType() == WhatThermoType.MANUAL) {
633                     // manual setpoint frame should be like *4*110#TTTT*#0##
634                     newTemp = Thermoregulation.decodeTemperature(parameters[0]);
635                     logger.debug("updateSetpoint() parsed temperature from {}: {} ---> {}", tmsg.toStringVerbose(),
636                             parameters[0], newTemp);
637                 }
638             } else {
639                 newTemp = Thermoregulation.parseTemperature(tmsg);
640             }
641             if (newTemp > 0) {
642                 updateState(CHANNEL_TEMP_SETPOINT, getAsQuantityTypeOrNull(newTemp, SIUnits.CELSIUS));
643                 currentSetPointTemp = newTemp;
644             }
645         } catch (NumberFormatException e) {
646             logger.warn("updateSetpoint() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
647             updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
648         } catch (FrameException e) {
649             logger.warn("updateSetpoint() FrameException on frame {}: {}", tmsg, e.getMessage());
650             updateState(CHANNEL_TEMP_SETPOINT, UnDefType.UNDEF);
651         }
652     }
653
654     private void updateFanCoilSpeed(Thermoregulation tmsg) {
655         try {
656             Thermoregulation.FanCoilSpeed speed = Thermoregulation.parseFanCoilSpeed(tmsg);
657             updateState(CHANNEL_FAN_SPEED, new StringType(speed.toString()));
658         } catch (NumberFormatException e) {
659             logger.warn("updateFanCoilSpeed() NumberFormatException on frame {}: {}", tmsg, e.getMessage());
660             updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
661         } catch (FrameException e) {
662             logger.warn("updateFanCoilSpeed() FrameException on frame {}: {}", tmsg, e.getMessage());
663             updateState(CHANNEL_FAN_SPEED, UnDefType.UNDEF);
664         }
665     }
666
667     private void updateValveStatus(Thermoregulation tmsg) {
668         try {
669             Thermoregulation.ValveOrActuatorStatus cv = Thermoregulation.parseValveStatus(tmsg,
670                     Thermoregulation.WhatThermoType.CONDITIONING);
671             updateState(CHANNEL_CONDITIONING_VALVES, new StringType(cv.toString()));
672             updateHeatingCooling(false, cv);
673
674             Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseValveStatus(tmsg,
675                     Thermoregulation.WhatThermoType.HEATING);
676             updateState(CHANNEL_HEATING_VALVES, new StringType(hv.toString()));
677             updateHeatingCooling(true, hv);
678
679         } catch (FrameException e) {
680             logger.warn("updateValveStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
681             updateState(CHANNEL_CONDITIONING_VALVES, UnDefType.UNDEF);
682             updateState(CHANNEL_HEATING_VALVES, UnDefType.UNDEF);
683         }
684     }
685
686     private void updateHeatingCooling(boolean heating, Thermoregulation.ValveOrActuatorStatus v) {
687         boolean state = false;
688         switch (v) {
689             case STOP:
690             case CLOSED:
691             case OFF:
692             case OFF_FAN_COIL:
693             case OFF_SPEED_1:
694             case OFF_SPEED_2:
695             case OFF_SPEED_3:
696                 state = false;
697                 break;
698             case ON:
699             case ON_FAN_COIL:
700             case ON_SPEED_1:
701             case ON_SPEED_2:
702             case ON_SPEED_3:
703             case OPENED:
704                 state = true;
705                 break;
706         }
707
708         if (heating) {
709             updateState(CHANNEL_HEATING, OnOffType.from(state));
710         } else {
711             updateState(CHANNEL_COOLING, OnOffType.from(state));
712         }
713     }
714
715     private void updateActuatorStatus(Thermoregulation tmsg) {
716         try {
717             Thermoregulation.ValveOrActuatorStatus hv = Thermoregulation.parseActuatorStatus(tmsg);
718             updateState(CHANNEL_ACTUATORS, new StringType(hv.toString()));
719         } catch (FrameException e) {
720             logger.warn("updateActuatorStatus() FrameException on frame {}: {}", tmsg, e.getMessage());
721             updateState(CHANNEL_ACTUATORS, UnDefType.UNDEF);
722         }
723     }
724
725     private void updateLocalOffset(Thermoregulation tmsg) {
726         try {
727             Thermoregulation.LocalOffset offset = Thermoregulation.parseLocalOffset(tmsg);
728             updateState(CHANNEL_LOCAL_OFFSET, new StringType(offset.toString()));
729             logger.debug("updateLocalOffset() {}: {}", tmsg, offset.toString());
730
731         } catch (FrameException e) {
732             logger.warn("updateLocalOffset() FrameException on frame {}: {}", tmsg, e.getMessage());
733             updateState(CHANNEL_LOCAL_OFFSET, UnDefType.UNDEF);
734         }
735     }
736
737     private void updateCURemoteControlStatus(String status) {
738         updateState(CHANNEL_CU_REMOTE_CONTROL, new StringType(status));
739         logger.debug("updateCURemoteControlStatus(): {}", status);
740     }
741
742     private void resetCUState() {
743         logger.debug("########### resetting CU state");
744         cuAtLeastOneProbeOff = false;
745         cuAtLeastOneProbeProtection = false;
746         cuAtLeastOneProbeManual = false;
747         cuBatteryStatus = CU_BATTERY_OK;
748         cuFailureDiscovered = false;
749
750         cuStateChannelsUpdateSchedule = scheduler.schedule(() -> {
751             updateCUStateChannels();
752         }, CU_STATE_CHANNELS_UPDATE_DELAY, TimeUnit.MILLISECONDS);
753     }
754
755     private void updateCUStateChannels() {
756         updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_OFF, OnOffType.from(cuAtLeastOneProbeOff));
757         updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_PROTECTION, OnOffType.from(cuAtLeastOneProbeProtection));
758         updateState(CHANNEL_CU_AT_LEAST_ONE_PROBE_MANUAL, OnOffType.from(cuAtLeastOneProbeManual));
759         updateState(CHANNEL_CU_BATTERY_STATUS, new StringType(cuBatteryStatus));
760         updateState(CHANNEL_CU_FAILURE_DISCOVERED, OnOffType.from(cuFailureDiscovered));
761     }
762
763     private Boolean channelExists(String channelID) {
764         return thing.getChannel(channelID) != null;
765     }
766
767     @Override
768     protected void refreshDevice(boolean refreshAll) {
769         logger.debug("--- refreshDevice() : refreshing SINGLE... ({})", thing.getUID());
770
771         Where w = deviceWhere;
772         if (w == null) {
773             logger.warn("refreshDevice() Where is null for thing {}", getThing().getUID());
774             return;
775         }
776
777         String whereStr = w.value();
778
779         if (isCentralUnit) {
780             try {
781                 send(Thermoregulation.requestStatus(getWhere(whereStr)));
782             } catch (OWNException e) {
783                 logger.warn("refreshDevice() central unit returned OWNException {}", e.getMessage());
784             }
785             return;
786         }
787
788         try {
789             send(Thermoregulation.requestTemperature(whereStr));
790
791             if (!((WhereThermo) w).isProbe()) {
792                 // for bus_thermo_zone request also other single channels updates
793                 send(Thermoregulation.requestSetPointTemperature(whereStr));
794                 send(Thermoregulation.requestMode(whereStr));
795
796                 // refresh ONLY subscribed channels
797                 if (channelExists(CHANNEL_FAN_SPEED)) {
798                     send(Thermoregulation.requestFanCoilSpeed(whereStr));
799                 }
800                 if (channelExists(CHANNEL_CONDITIONING_VALVES) || channelExists(CHANNEL_HEATING_VALVES)) {
801                     send(Thermoregulation.requestValvesStatus(whereStr));
802                 }
803                 if (channelExists(CHANNEL_ACTUATORS)) {
804                     send(Thermoregulation.requestActuatorsStatus(whereStr));
805                 }
806                 if (channelExists(CHANNEL_LOCAL_OFFSET)) {
807                     send(Thermoregulation.requestLocalOffset(whereStr));
808                 }
809             }
810         } catch (OWNException e) {
811             logger.warn("refreshDevice() where='{}' returned OWNException {}", whereStr, e.getMessage());
812         }
813     }
814
815     @Override
816     public void dispose() {
817         ScheduledFuture<?> s = cuStateChannelsUpdateSchedule;
818         if (s != null) {
819             s.cancel(false);
820             logger.debug("dispose() - scheduler stopped.");
821         }
822         super.dispose();
823     }
824 }