]> git.basschouten.com Git - openhab-addons.git/blob
d77d146ca7f59106e17b64e055c79f51f227839e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.modbus.helioseasycontrols.internal;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.lang.reflect.Type;
19 import java.nio.charset.StandardCharsets;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.Semaphore;
29 import java.util.concurrent.TimeUnit;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.unit.SIUnits;
40 import org.openhab.core.library.unit.SmartHomeUnits;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.openhab.io.transport.modbus.ModbusBitUtilities;
54 import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
55 import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
56 import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
57 import org.openhab.io.transport.modbus.ModbusRegister;
58 import org.openhab.io.transport.modbus.ModbusRegisterArray;
59 import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
60 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 import com.google.gson.Gson;
65 import com.google.gson.reflect.TypeToken;
66
67 /**
68  * The {@link HeliosEasyControlsHandler} is responsible for handling commands, which are
69  * sent to one of the channels.
70  *
71  * @author Bernhard Bauer - Initial contribution
72  */
73 @NonNullByDefault
74 public class HeliosEasyControlsHandler extends BaseThingHandler {
75
76     private final Logger logger = LoggerFactory.getLogger(HeliosEasyControlsHandler.class);
77
78     private @Nullable HeliosEasyControlsConfiguration config;
79
80     private @Nullable ScheduledFuture<?> pollingJob;
81
82     private @Nullable Map<String, HeliosVariable> variableMap;
83
84     /**
85      * This flag is used to ensure read requests (consisting of a write and subsequent read) are not influenced by
86      * another transaction
87      */
88     private final Map<ModbusSlaveEndpoint, Semaphore> transactionLocks = new ConcurrentHashMap<>();
89
90     private final Gson gson = new Gson();
91
92     private @Nullable ModbusCommunicationInterface comms;
93
94     private int dateFormat = -1;
95     private ZonedDateTime sysDate = ZonedDateTime.now(); // initialize with local system time as a best guess
96                                                          // before reading from device
97
98     private class BypassDate {
99         // initialization to avoid issues when updating before all variables were read
100         private int month = 1;
101         private int day = 1;
102
103         public void setMonth(int month) {
104             this.month = month;
105         }
106
107         public void setDay(int day) {
108             this.day = day;
109         }
110
111         public DateTimeType toDateTimeType() {
112             return new DateTimeType(ZonedDateTime.of(1900, this.month, this.day, 0, 0, 0, 0, ZoneId.of("UTC+00:00")));
113         }
114     }
115
116     private @Nullable BypassDate bypassFrom, bypassTo;
117
118     public HeliosEasyControlsHandler(Thing thing) {
119         super(thing);
120     }
121
122     /**
123      * Reads variable definitions from JSON file and store them in variableMap
124      */
125     private void readVariableDefinition() {
126         Type vMapType = new TypeToken<Map<String, HeliosVariable>>() {
127         }.getType();
128         try (InputStreamReader jsonFile = new InputStreamReader(
129                 getClass().getResourceAsStream(HeliosEasyControlsBindingConstants.VARIABLES_DEFINITION_FILE));
130                 BufferedReader reader = new BufferedReader(jsonFile)) {
131             this.variableMap = gson.fromJson(reader, vMapType);
132         } catch (IOException e) {
133             this.handleError("Error reading variable definition file", ThingStatusDetail.CONFIGURATION_ERROR);
134         }
135         if (variableMap != null) {
136             // add the name to the variable itself
137             for (Map.Entry<String, HeliosVariable> entry : this.variableMap.entrySet()) {
138                 entry.getValue().setName(entry.getKey()); // workaround to set the variable name inside the
139                                                           // HeliosVariable object
140                 if (!entry.getValue().isOk()) {
141                     this.handleError("Variables definition file contains inconsistent data",
142                             ThingStatusDetail.CONFIGURATION_ERROR);
143                 }
144             }
145         } else {
146             this.handleError("Variables definition file not found or of illegal format",
147                     ThingStatusDetail.CONFIGURATION_ERROR);
148         }
149     }
150
151     /**
152      * Get the endpoint handler from the bridge this handler is connected to
153      * Checks that we're connected to the right type of bridge
154      *
155      * @return the endpoint handler or null if the bridge does not exist
156      */
157     private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
158         Bridge bridge = getBridge();
159         if (bridge == null) {
160             logger.debug("Bridge is null");
161             return null;
162         }
163         if (bridge.getStatus() != ThingStatus.ONLINE) {
164             logger.debug("Bridge is not online");
165             return null;
166         }
167
168         ThingHandler handler = bridge.getHandler();
169         if (handler == null) {
170             logger.debug("Bridge handler is null");
171             return null;
172         }
173
174         if (handler instanceof ModbusEndpointThingHandler) {
175             return (ModbusEndpointThingHandler) handler;
176         } else {
177             logger.debug("Unexpected bridge handler: {}", handler);
178             return null;
179         }
180     }
181
182     /**
183      * Get a reference to the modbus endpoint
184      */
185     private void connectEndpoint() {
186         if (this.comms != null) {
187             return;
188         }
189
190         ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
191         if (slaveEndpointThingHandler == null) {
192             @SuppressWarnings("null")
193             String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
194             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
195                     String.format("Bridge '%s' is offline", label));
196             logger.debug("No bridge handler available -- aborting init for {}", label);
197             return;
198         }
199
200         comms = slaveEndpointThingHandler.getCommunicationInterface();
201
202         if (comms == null) {
203             @SuppressWarnings("null")
204             String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
205             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
206                     String.format("Bridge '%s' not completely initialized", label));
207             logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
208             return;
209         }
210     }
211
212     @Override
213     public void initialize() {
214         this.config = getConfigAs(HeliosEasyControlsConfiguration.class);
215         this.readVariableDefinition();
216         this.connectEndpoint();
217         if ((this.comms != null) && (this.variableMap != null) && (this.config != null)) {
218             this.transactionLocks.putIfAbsent(this.comms.getEndpoint(), new Semaphore(1, true));
219             updateStatus(ThingStatus.UNKNOWN);
220
221             // background initialization
222             scheduler.execute(() -> {
223                 readValue(HeliosEasyControlsBindingConstants.DATE_FORMAT);
224                 // status will be updated to ONLINE by the read callback function (via processResponse)
225             });
226
227             // poll for status updates regularly
228             HeliosEasyControlsConfiguration config = this.config;
229             if (config != null) {
230                 this.pollingJob = scheduler.scheduleWithFixedDelay(() -> {
231                     if (variableMap != null) {
232                         for (Map.Entry<String, HeliosVariable> entry : variableMap.entrySet()) {
233                             if (this.isProperty(entry.getKey()) || isLinked(entry.getValue().getGroupAndName())
234                                     || HeliosEasyControlsBindingConstants.ALWAYS_UPDATE_VARIABLES
235                                             .contains(entry.getKey())) {
236                                 readValue(entry.getKey());
237                             }
238                         }
239                     } else {
240                         handleError("Variable definition is null", ThingStatusDetail.CONFIGURATION_ERROR);
241                     }
242                 }, config.getRefreshInterval(), config.getRefreshInterval(), TimeUnit.MILLISECONDS);
243             }
244         } else { // at least one null assertion has failed, let's log the problem and update the thing status
245             if (this.comms == null) {
246                 this.handleError("Modbus communication interface is unavailable",
247                         ThingStatusDetail.COMMUNICATION_ERROR);
248             }
249             if (this.variableMap == null) {
250                 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
251             }
252             if (this.config == null) {
253                 this.handleError("Binding configuration is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
254             }
255         }
256     }
257
258     @Override
259     public void dispose() {
260         if (this.pollingJob != null) {
261             this.pollingJob.cancel(true);
262         }
263         this.comms = null;
264     }
265
266     @Override
267     public void handleCommand(ChannelUID channelUID, Command command) {
268         String channelId = channelUID.getIdWithoutGroup();
269         if (command instanceof RefreshType) {
270             if (channelId.equals(HeliosEasyControlsBindingConstants.SYS_DATE)) {
271                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.DATE));
272                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.TIME));
273             } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_FROM)) {
274                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY));
275                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH));
276             } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_TO)) {
277                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY));
278                 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH));
279             } else {
280                 scheduler.submit(() -> readValue(channelId));
281             }
282         } else { // write command
283             String value = null;
284             if (command instanceof OnOffType) {
285                 value = command == OnOffType.ON ? "1" : "0";
286             } else if (command instanceof DateTimeType) {
287                 ZonedDateTime d = ((DateTimeType) command).getZonedDateTime();
288                 if (channelId.equals(HeliosEasyControlsBindingConstants.SYS_DATE)) {
289                     setSysDateTime(d);
290                 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_FROM)) {
291                     this.setBypass(true, d.getDayOfMonth(), d.getMonthValue());
292                 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_TO)) {
293                     this.setBypass(false, d.getDayOfMonth(), d.getMonthValue());
294                 } else {
295                     value = formatDate(channelId, ((DateTimeType) command).getZonedDateTime());
296                 }
297             } else if ((command instanceof DecimalType) || (command instanceof StringType)) {
298                 value = command.toString();
299             } else if (command instanceof QuantityType<?>) {
300                 // convert item's unit to the Helios device's unit
301                 Map<String, HeliosVariable> variableMap = this.variableMap;
302                 if (variableMap != null) {
303                     String unit = variableMap.get(channelId).getUnit();
304                     QuantityType<?> val = (QuantityType<?>) command;
305                     if (unit != null) {
306                         switch (unit) {
307                             case HeliosVariable.UNIT_DAY:
308                                 val = val.toUnit(SmartHomeUnits.DAY);
309                                 break;
310                             case HeliosVariable.UNIT_HOUR:
311                                 val = val.toUnit(SmartHomeUnits.HOUR);
312                                 break;
313                             case HeliosVariable.UNIT_MIN:
314                                 val = val.toUnit(SmartHomeUnits.MINUTE);
315                                 break;
316                             case HeliosVariable.UNIT_SEC:
317                                 val = val.toUnit(SmartHomeUnits.SECOND);
318                                 break;
319                             case HeliosVariable.UNIT_VOLT:
320                                 val = val.toUnit(SmartHomeUnits.VOLT);
321                                 break;
322                             case HeliosVariable.UNIT_PERCENT:
323                                 val = val.toUnit(SmartHomeUnits.PERCENT);
324                                 break;
325                             case HeliosVariable.UNIT_PPM:
326                                 val = val.toUnit(SmartHomeUnits.PARTS_PER_MILLION);
327                                 break;
328                             case HeliosVariable.UNIT_TEMP:
329                                 val = val.toUnit(SIUnits.CELSIUS);
330                                 break;
331                         }
332                         value = val != null ? String.valueOf(val.doubleValue()) : null; // ignore the UoM
333                     }
334                 }
335             }
336             if (value != null) {
337                 final String v = value;
338                 scheduler.submit(() -> {
339                     try {
340                         writeValue(channelId, v);
341                         if (variableMap != null) {
342                             updateState(variableMap.get(channelId), v);
343                             updateStatus(ThingStatus.ONLINE);
344                         }
345                     } catch (HeliosException e) {
346                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
347                                 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
348                     }
349                 });
350             }
351         }
352     }
353
354     @Override
355     public Collection<Class<? extends ThingHandlerService>> getServices() {
356         return Collections.singleton(HeliosEasyControlsActions.class);
357     }
358
359     /**
360      * Checks if the provided variable name is a property
361      *
362      * @param variableName The variable's name
363      * @return true if the variable is a property
364      */
365     private boolean isProperty(String variableName) {
366         return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
367     }
368
369     /**
370      * Writes a variable value to the Helios device
371      *
372      * @param variableName The variable name
373      * @param value The new value
374      * @return The value if the transaction succeeded, <tt>null</tt> otherwise
375      * @throws HeliosException Thrown if the variable is read-only or the provided value is out of range
376      */
377     public void writeValue(String variableName, String value) throws HeliosException {
378         if (this.variableMap == null) {
379             this.handleError("Variable definition is unavailable.", ThingStatusDetail.CONFIGURATION_ERROR);
380             return;
381         } else {
382             Map<String, HeliosVariable> variableMap = this.variableMap;
383             if (variableMap != null) {
384                 HeliosVariable v = variableMap.get(variableName);
385
386                 if (!v.hasWriteAccess()) {
387                     throw new HeliosException("Variable " + variableName + " is read-only");
388                 } else if (!v.isInAllowedRange(value)) {
389                     throw new HeliosException(
390                             "Value " + value + " is outside of allowed range of variable " + variableName);
391                 } else if (this.comms != null) {
392                     // write to device
393                     String payload = v.getVariableString() + "=" + value;
394                     ModbusCommunicationInterface comms = this.comms;
395                     if (comms != null) {
396                         final Semaphore lock = transactionLocks.get(comms.getEndpoint());
397                         try {
398                             lock.acquire();
399                             comms.submitOneTimeWrite(
400                                     new ModbusWriteRegisterRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
401                                             HeliosEasyControlsBindingConstants.START_ADDRESS,
402                                             new ModbusRegisterArray(preparePayload(payload)), true,
403                                             HeliosEasyControlsBindingConstants.MAX_TRIES),
404                                     result -> {
405                                         lock.release();
406                                         updateStatus(ThingStatus.ONLINE);
407                                     }, failureInfo -> {
408                                         lock.release();
409                                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
410                                                 "Error writing to device: " + failureInfo.getCause().getMessage());
411                                     });
412                         } catch (InterruptedException e) {
413                             logger.warn(
414                                     "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
415                                     HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
416                         }
417                     }
418                 } else { // comms is null
419                     this.handleError("Modbus communication interface is null", ThingStatusDetail.COMMUNICATION_ERROR);
420                 }
421             }
422         }
423     }
424
425     /**
426      * Read a variable from the Helios device
427      *
428      * @param variableName The variable name
429      * @return The value
430      */
431     public void readValue(String variableName) {
432         Map<String, HeliosVariable> variableMap = this.variableMap;
433         ModbusCommunicationInterface comms = this.comms;
434         if ((comms != null) && (variableMap != null)) {
435             final Semaphore lock = transactionLocks.get(comms.getEndpoint());
436             HeliosVariable v = variableMap.get(variableName);
437             if (v.hasReadAccess()) {
438                 try {
439                     lock.acquire(); // will block until lock is available
440                 } catch (InterruptedException e) {
441                     logger.warn("{} encountered Exception when trying to read variable {} from the device: {}",
442                             HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
443                     return;
444                 }
445                 // write variable name to register
446                 String payload = v.getVariableString();
447                 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
448                         HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS,
449                         new ModbusRegisterArray(preparePayload(payload)), true,
450                         HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
451                             comms.submitOneTimePoll(
452                                     new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
453                                             ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
454                                             HeliosEasyControlsBindingConstants.START_ADDRESS, v.getCount(),
455                                             HeliosEasyControlsBindingConstants.MAX_TRIES),
456                                     pollResult -> {
457                                         lock.release();
458                                         Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
459                                         if (registers.isPresent()) {
460                                             processResponse(v, registers.get());
461                                         }
462                                     }, failureInfo -> {
463                                         lock.release();
464                                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
465                                                 "Error reading from device: " + failureInfo.getCause().getMessage());
466                                     });
467                         }, failureInfo -> {
468                             lock.release();
469                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
470                                     "Error writing to device: " + failureInfo.getCause().getMessage());
471
472                         });
473             }
474
475         } else {
476             if (this.comms == null) {
477                 this.handleError("Modbus communication interface is unavailable",
478                         ThingStatusDetail.COMMUNICATION_ERROR);
479             }
480             if (variableMap == null) {
481                 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
482             }
483         }
484     }
485
486     private void updateSysDate(DateTimeType dateTime) {
487         this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
488     }
489
490     private void updateSysTime(DateTimeType dateTime) {
491         this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
492     }
493
494     private void updateUtcOffset(int utcOffset) {
495         this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
496     }
497
498     private void updateSysDateTime(ZonedDateTime dateTime, boolean updateDate, int utcOffset) {
499         ZonedDateTime sysDate = this.sysDate;
500         sysDate = ZonedDateTime.of(updateDate ? dateTime.getYear() : sysDate.getYear(),
501                 updateDate ? dateTime.getMonthValue() : sysDate.getMonthValue(),
502                 updateDate ? dateTime.getDayOfMonth() : sysDate.getDayOfMonth(),
503                 updateDate ? sysDate.getHour() : dateTime.getHour(),
504                 updateDate ? sysDate.getMinute() : dateTime.getMinute(),
505                 updateDate ? sysDate.getSecond() : dateTime.getSecond(), 0,
506                 ZoneId.of("UTC" + (utcOffset >= 0 ? "+" : "") + String.format("%02d", utcOffset) + ":00"));
507         updateState("general#" + HeliosEasyControlsBindingConstants.SYS_DATE, new DateTimeType(sysDate));
508         this.sysDate = sysDate;
509     }
510
511     private void setSysDateTime(ZonedDateTime date) {
512         try {
513             this.writeValue(HeliosEasyControlsBindingConstants.DATE,
514                     this.formatDate(HeliosEasyControlsBindingConstants.DATE, date));
515             this.writeValue(HeliosEasyControlsBindingConstants.TIME,
516                     date.getHour() + ":" + date.getMinute() + ":" + date.getSecond());
517             this.writeValue(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT,
518                     Integer.toString(date.getOffset().getTotalSeconds() / 60 / 60));
519         } catch (HeliosException e) {
520             logger.warn("{} encountered Exception when trying to set system date: {}",
521                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
522         }
523     }
524
525     protected void setSysDateTime() {
526         this.setSysDateTime(ZonedDateTime.now());
527     }
528
529     private void updateBypass(boolean from, boolean month, int val) {
530         BypassDate bypassDate = from ? this.bypassFrom : this.bypassTo;
531         if (bypassDate == null) {
532             bypassDate = new BypassDate();
533         }
534         if (month) {
535             bypassDate.setMonth(val);
536
537         } else {
538             bypassDate.setDay(val);
539         }
540         updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
541                 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
542         if (from) {
543             this.bypassFrom = bypassDate;
544
545         } else {
546             this.bypassTo = bypassDate;
547         }
548     }
549
550     protected void setBypass(boolean from, int day, int month) {
551         try {
552             this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
553                     : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(day));
554             this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH
555                     : HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH, Integer.toString(month));
556         } catch (HeliosException e) {
557             logger.warn("{} encountered Exception when trying to set bypass period: {}",
558                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
559         }
560     }
561
562     /**
563      * Formats the provided date to a string in the device's configured date format
564      *
565      * @param variableName the variable name
566      * @param date the date to be formatted
567      * @return a string in the device's configured date format
568      */
569     public String formatDate(String variableName, ZonedDateTime date) {
570         String y = Integer.toString(date.getYear());
571         String m = Integer.toString(date.getMonthValue());
572         if (m.length() == 1) {
573             m = "0" + m;
574         }
575         String d = Integer.toString(date.getDayOfMonth());
576         if (d.length() == 1) {
577             d = "0" + d;
578         }
579         if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
580             return d + "." + m + "." + y;
581         } else {
582             switch (this.dateFormat) {
583                 case 0: // dd.mm.yyyy
584                     return d + "." + m + "." + y;
585                 case 1: // mm.dd.yyyy
586                     return m + "." + d + "." + y;
587                 case 2: // yyyy.mm.dd
588                     return y + "." + m + "." + d;
589                 default:
590                     return d + "." + m + "." + y;
591             }
592         }
593     }
594
595     /**
596      * Returns a DateTimeType object based on the provided String and the device's configured date format
597      *
598      * @param date The date string read from the device
599      * @return A DateTimeType object representing the date or time specified
600      */
601     private DateTimeType toDateTime(String date) {
602         String[] dateTimeParts = null;
603         String dateTime = date;
604         dateTimeParts = date.split("\\."); // try to split date components
605         if (dateTimeParts.length == 1) { // time
606             return DateTimeType.valueOf(date);
607         } else if (dateTimeParts.length == 3) { // date - we'll try the device's date format
608             switch (this.dateFormat) {
609                 case 0: // dd.mm.yyyy
610                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
611                     break;
612                 case 1: // mm.dd.yyyy
613                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
614                     break;
615                 case 2: // yyyy.mm.dd
616                     dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
617                     break;
618                 default:
619                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
620                     break;
621             }
622             return DateTimeType.valueOf(dateTime);
623         }
624         // falling back to default date format (apparently using the configured format has failed)
625         dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
626         return DateTimeType.valueOf(dateTime);
627     }
628
629     private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
630         if (unit == null) {
631             return null;
632         } else if (unit.equals(HeliosVariable.UNIT_DAY)) {
633             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.DAY);
634         } else if (unit.equals(HeliosVariable.UNIT_HOUR)) {
635             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.HOUR);
636         } else if (unit.equals(HeliosVariable.UNIT_MIN)) {
637             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.MINUTE);
638         } else if (unit.equals(HeliosVariable.UNIT_SEC)) {
639             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.SECOND);
640         } else if (unit.equals(HeliosVariable.UNIT_VOLT)) {
641             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.VOLT);
642         } else if (unit.equals(HeliosVariable.UNIT_PERCENT)) {
643             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PERCENT);
644         } else if (unit.equals(HeliosVariable.UNIT_PPM)) {
645             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PARTS_PER_MILLION);
646         } else if (unit.equals(HeliosVariable.UNIT_TEMP)) {
647             return new QuantityType<>(Float.parseFloat(value), SIUnits.CELSIUS);
648         } else {
649             return null;
650         }
651     }
652
653     /**
654      * Prepares the payload for the request
655      *
656      * @param payload The String representation of the payload
657      * @return The Register representation of the payload
658      */
659     private ModbusRegister[] preparePayload(String payload) {
660
661         // determine number of registers
662         int l = (payload.length() + 1) / 2; // +1 because we need to include at least one termination symbol 0x00
663         if ((payload.length() + 1) % 2 != 0) {
664             l++;
665         }
666
667         ModbusRegister reg[] = new ModbusRegister[l];
668         byte[] b = payload.getBytes();
669         int ch = 0;
670         for (int i = 0; i < reg.length; i++) {
671             byte b1 = ch < b.length ? b[ch] : (byte) 0x00; // terminate with 0x00 if at the end of the payload
672             ch++;
673             byte b2 = ch < b.length ? b[ch] : (byte) 0x00;
674             ch++;
675             reg[i] = new ModbusRegister(b1, b2);
676         }
677         return reg;
678     }
679
680     /**
681      * Decodes the Helios device' response and updates the channel with the actual value of the variable
682      *
683      * @param response The registers received from the Helios device
684      * @return The value or <tt>null</tt> if an error occurred
685      */
686     private void processResponse(HeliosVariable v, ModbusRegisterArray registers) {
687         String r = ModbusBitUtilities
688                 .extractStringFromRegisters(registers, 0, registers.size() * 2, StandardCharsets.US_ASCII).toString();
689         String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string
690         // making sure we have a proper response and the response matches the requested variable
691         if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) {
692             if (this.isProperty(v.getName())) {
693                 try {
694                     updateProperty(v.getName(), v.formatPropertyValue(parts[1]));
695                 } catch (HeliosException e) {
696                     logger.warn("{} encountered Exception when trying to update property: {}",
697                             HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
698                 }
699             } else {
700                 this.updateState(v, parts[1]);
701             }
702         } else { // another variable was read
703             logger.warn("{} tried to read value from variable {} and the result provided by the device was {}",
704                     HeliosEasyControlsHandler.class.getSimpleName(), v.getName(), r);
705         }
706     }
707
708     private void updateState(HeliosVariable v, String value) {
709         String variableType = v.getType();
710         // System date and time
711         if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE)) {
712             this.updateSysDate(this.toDateTime(value));
713         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME)) {
714             this.updateSysTime(this.toDateTime(value));
715         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT)) {
716             this.updateUtcOffset(Integer.parseInt(value));
717             // Bypass
718         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY)) {
719             this.updateBypass(true, false, Integer.parseInt(value));
720         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH)) {
721             this.updateBypass(true, true, Integer.parseInt(value));
722         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY)) {
723             this.updateBypass(false, false, Integer.parseInt(value));
724         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH)) {
725             this.updateBypass(false, true, Integer.parseInt(value));
726         } else {
727             Channel channel = getThing().getChannel(v.getGroupAndName());
728             String itemType;
729             if (channel != null) {
730                 itemType = channel.getAcceptedItemType();
731                 if (itemType != null) {
732                     if (itemType.startsWith("Number:")) {
733                         itemType = "Number";
734                     }
735                     switch (itemType) {
736                         case "Number":
737                             if (((variableType.equals(HeliosVariable.TYPE_INTEGER))
738                                     || (variableType == HeliosVariable.TYPE_FLOAT)) && (!value.equals("-"))) {
739                                 State state = null;
740                                 if (v.getUnit() == null) {
741                                     state = DecimalType.valueOf(value);
742                                 } else { // QuantityType
743                                     state = this.toQuantityType(value, v.getUnit());
744                                 }
745                                 if (state != null) {
746                                     updateState(v.getGroupAndName(), state);
747                                     updateStatus(ThingStatus.ONLINE);
748                                     // update date format and UTC offset upon read
749                                     if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE_FORMAT)) {
750                                         this.dateFormat = Integer.parseInt(value);
751                                     }
752                                 }
753                             }
754                             break;
755                         case "Switch":
756                             if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
757                                 updateState(v.getGroupAndName(), value.equals("1") ? OnOffType.ON : OnOffType.OFF);
758                             }
759                             break;
760                         case "String":
761                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
762                                 updateState(v.getGroupAndName(), StringType.valueOf(value));
763                             }
764                             break;
765                         case "DateTime":
766                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
767                                 updateState(v.getGroupAndName(), toDateTime(value));
768                             }
769                             break;
770                     }
771                 } else { // itemType was null
772                     logger.warn("{} couldn't determine item type of variable {}",
773                             HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
774                 }
775             } else { // channel was null
776                 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
777                         v.getName());
778             }
779         }
780     }
781
782     /**
783      * Logs an error (as a warning entry) and updates the thing status
784      *
785      * @param errorMsg The error message to be logged and provided with the Thing's status update
786      * @param status The Thing's new status
787      */
788     private void handleError(String errorMsg, ThingStatusDetail status) {
789         updateStatus(ThingStatus.OFFLINE, status, errorMsg);
790     }
791 }