]> git.basschouten.com Git - openhab-addons.git/blob
4d0da544e1dd00d38d41d5454b2b3bd274c711cc
[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                             HeliosVariable variable = variableMap.get(channelId);
343                             if (variable != null) {
344                                 updateState(variable, v);
345                                 updateStatus(ThingStatus.ONLINE);
346                             }
347                         }
348                     } catch (HeliosException e) {
349                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
350                                 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
351                     }
352                 });
353             }
354         }
355     }
356
357     @Override
358     public Collection<Class<? extends ThingHandlerService>> getServices() {
359         return Collections.singleton(HeliosEasyControlsActions.class);
360     }
361
362     /**
363      * Checks if the provided variable name is a property
364      *
365      * @param variableName The variable's name
366      * @return true if the variable is a property
367      */
368     private boolean isProperty(String variableName) {
369         return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
370     }
371
372     /**
373      * Writes a variable value to the Helios device
374      *
375      * @param variableName The variable name
376      * @param value The new value
377      * @return The value if the transaction succeeded, <tt>null</tt> otherwise
378      * @throws HeliosException Thrown if the variable is read-only or the provided value is out of range
379      */
380     public void writeValue(String variableName, String value) throws HeliosException {
381         if (this.variableMap == null) {
382             this.handleError("Variable definition is unavailable.", ThingStatusDetail.CONFIGURATION_ERROR);
383             return;
384         } else {
385             Map<String, HeliosVariable> variableMap = this.variableMap;
386             if (variableMap != null) {
387                 HeliosVariable v = variableMap.get(variableName);
388
389                 if (!v.hasWriteAccess()) {
390                     throw new HeliosException("Variable " + variableName + " is read-only");
391                 } else if (!v.isInAllowedRange(value)) {
392                     throw new HeliosException(
393                             "Value " + value + " is outside of allowed range of variable " + variableName);
394                 } else if (this.comms != null) {
395                     // write to device
396                     String payload = v.getVariableString() + "=" + value;
397                     ModbusCommunicationInterface comms = this.comms;
398                     if (comms != null) {
399                         final Semaphore lock = transactionLocks.get(comms.getEndpoint());
400                         try {
401                             lock.acquire();
402                             comms.submitOneTimeWrite(
403                                     new ModbusWriteRegisterRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
404                                             HeliosEasyControlsBindingConstants.START_ADDRESS,
405                                             new ModbusRegisterArray(preparePayload(payload)), true,
406                                             HeliosEasyControlsBindingConstants.MAX_TRIES),
407                                     result -> {
408                                         lock.release();
409                                         updateStatus(ThingStatus.ONLINE);
410                                     }, failureInfo -> {
411                                         lock.release();
412                                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
413                                                 "Error writing to device: " + failureInfo.getCause().getMessage());
414                                     });
415                         } catch (InterruptedException e) {
416                             logger.warn(
417                                     "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
418                                     HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
419                         }
420                     }
421                 } else { // comms is null
422                     this.handleError("Modbus communication interface is null", ThingStatusDetail.COMMUNICATION_ERROR);
423                 }
424             }
425         }
426     }
427
428     /**
429      * Read a variable from the Helios device
430      *
431      * @param variableName The variable name
432      * @return The value
433      */
434     public void readValue(String variableName) {
435         Map<String, HeliosVariable> variableMap = this.variableMap;
436         ModbusCommunicationInterface comms = this.comms;
437         if ((comms != null) && (variableMap != null)) {
438             final Semaphore lock = transactionLocks.get(comms.getEndpoint());
439             HeliosVariable v = variableMap.get(variableName);
440             if (v.hasReadAccess()) {
441                 try {
442                     lock.acquire(); // will block until lock is available
443                 } catch (InterruptedException e) {
444                     logger.warn("{} encountered Exception when trying to read variable {} from the device: {}",
445                             HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
446                     return;
447                 }
448                 // write variable name to register
449                 String payload = v.getVariableString();
450                 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
451                         HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS,
452                         new ModbusRegisterArray(preparePayload(payload)), true,
453                         HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
454                             comms.submitOneTimePoll(
455                                     new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
456                                             ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
457                                             HeliosEasyControlsBindingConstants.START_ADDRESS, v.getCount(),
458                                             HeliosEasyControlsBindingConstants.MAX_TRIES),
459                                     pollResult -> {
460                                         lock.release();
461                                         Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
462                                         if (registers.isPresent()) {
463                                             processResponse(v, registers.get());
464                                         }
465                                     }, failureInfo -> {
466                                         lock.release();
467                                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
468                                                 "Error reading from device: " + failureInfo.getCause().getMessage());
469                                     });
470                         }, failureInfo -> {
471                             lock.release();
472                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
473                                     "Error writing to device: " + failureInfo.getCause().getMessage());
474
475                         });
476             }
477
478         } else {
479             if (this.comms == null) {
480                 this.handleError("Modbus communication interface is unavailable",
481                         ThingStatusDetail.COMMUNICATION_ERROR);
482             }
483             if (variableMap == null) {
484                 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
485             }
486         }
487     }
488
489     private void updateSysDate(DateTimeType dateTime) {
490         this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
491     }
492
493     private void updateSysTime(DateTimeType dateTime) {
494         this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
495     }
496
497     private void updateUtcOffset(int utcOffset) {
498         this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
499     }
500
501     private void updateSysDateTime(ZonedDateTime dateTime, boolean updateDate, int utcOffset) {
502         ZonedDateTime sysDate = this.sysDate;
503         sysDate = ZonedDateTime.of(updateDate ? dateTime.getYear() : sysDate.getYear(),
504                 updateDate ? dateTime.getMonthValue() : sysDate.getMonthValue(),
505                 updateDate ? dateTime.getDayOfMonth() : sysDate.getDayOfMonth(),
506                 updateDate ? sysDate.getHour() : dateTime.getHour(),
507                 updateDate ? sysDate.getMinute() : dateTime.getMinute(),
508                 updateDate ? sysDate.getSecond() : dateTime.getSecond(), 0,
509                 ZoneId.of("UTC" + (utcOffset >= 0 ? "+" : "") + String.format("%02d", utcOffset) + ":00"));
510         updateState("general#" + HeliosEasyControlsBindingConstants.SYS_DATE, new DateTimeType(sysDate));
511         this.sysDate = sysDate;
512     }
513
514     private void setSysDateTime(ZonedDateTime date) {
515         try {
516             this.writeValue(HeliosEasyControlsBindingConstants.DATE,
517                     this.formatDate(HeliosEasyControlsBindingConstants.DATE, date));
518             this.writeValue(HeliosEasyControlsBindingConstants.TIME,
519                     date.getHour() + ":" + date.getMinute() + ":" + date.getSecond());
520             this.writeValue(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT,
521                     Integer.toString(date.getOffset().getTotalSeconds() / 60 / 60));
522         } catch (HeliosException e) {
523             logger.warn("{} encountered Exception when trying to set system date: {}",
524                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
525         }
526     }
527
528     protected void setSysDateTime() {
529         this.setSysDateTime(ZonedDateTime.now());
530     }
531
532     private void updateBypass(boolean from, boolean month, int val) {
533         BypassDate bypassDate = from ? this.bypassFrom : this.bypassTo;
534         if (bypassDate == null) {
535             bypassDate = new BypassDate();
536         }
537         if (month) {
538             bypassDate.setMonth(val);
539
540         } else {
541             bypassDate.setDay(val);
542         }
543         updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
544                 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
545         if (from) {
546             this.bypassFrom = bypassDate;
547
548         } else {
549             this.bypassTo = bypassDate;
550         }
551     }
552
553     protected void setBypass(boolean from, int day, int month) {
554         try {
555             this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
556                     : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(day));
557             this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH
558                     : HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH, Integer.toString(month));
559         } catch (HeliosException e) {
560             logger.warn("{} encountered Exception when trying to set bypass period: {}",
561                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
562         }
563     }
564
565     /**
566      * Formats the provided date to a string in the device's configured date format
567      *
568      * @param variableName the variable name
569      * @param date the date to be formatted
570      * @return a string in the device's configured date format
571      */
572     public String formatDate(String variableName, ZonedDateTime date) {
573         String y = Integer.toString(date.getYear());
574         String m = Integer.toString(date.getMonthValue());
575         if (m.length() == 1) {
576             m = "0" + m;
577         }
578         String d = Integer.toString(date.getDayOfMonth());
579         if (d.length() == 1) {
580             d = "0" + d;
581         }
582         if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
583             return d + "." + m + "." + y;
584         } else {
585             switch (this.dateFormat) {
586                 case 0: // dd.mm.yyyy
587                     return d + "." + m + "." + y;
588                 case 1: // mm.dd.yyyy
589                     return m + "." + d + "." + y;
590                 case 2: // yyyy.mm.dd
591                     return y + "." + m + "." + d;
592                 default:
593                     return d + "." + m + "." + y;
594             }
595         }
596     }
597
598     /**
599      * Returns a DateTimeType object based on the provided String and the device's configured date format
600      *
601      * @param date The date string read from the device
602      * @return A DateTimeType object representing the date or time specified
603      */
604     private DateTimeType toDateTime(String date) {
605         String[] dateTimeParts = null;
606         String dateTime = date;
607         dateTimeParts = date.split("\\."); // try to split date components
608         if (dateTimeParts.length == 1) { // time
609             return DateTimeType.valueOf(date);
610         } else if (dateTimeParts.length == 3) { // date - we'll try the device's date format
611             switch (this.dateFormat) {
612                 case 0: // dd.mm.yyyy
613                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
614                     break;
615                 case 1: // mm.dd.yyyy
616                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
617                     break;
618                 case 2: // yyyy.mm.dd
619                     dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
620                     break;
621                 default:
622                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
623                     break;
624             }
625             return DateTimeType.valueOf(dateTime);
626         }
627         // falling back to default date format (apparently using the configured format has failed)
628         dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
629         return DateTimeType.valueOf(dateTime);
630     }
631
632     private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
633         if (unit == null) {
634             return null;
635         } else if (unit.equals(HeliosVariable.UNIT_DAY)) {
636             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.DAY);
637         } else if (unit.equals(HeliosVariable.UNIT_HOUR)) {
638             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.HOUR);
639         } else if (unit.equals(HeliosVariable.UNIT_MIN)) {
640             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.MINUTE);
641         } else if (unit.equals(HeliosVariable.UNIT_SEC)) {
642             return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.SECOND);
643         } else if (unit.equals(HeliosVariable.UNIT_VOLT)) {
644             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.VOLT);
645         } else if (unit.equals(HeliosVariable.UNIT_PERCENT)) {
646             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PERCENT);
647         } else if (unit.equals(HeliosVariable.UNIT_PPM)) {
648             return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PARTS_PER_MILLION);
649         } else if (unit.equals(HeliosVariable.UNIT_TEMP)) {
650             return new QuantityType<>(Float.parseFloat(value), SIUnits.CELSIUS);
651         } else {
652             return null;
653         }
654     }
655
656     /**
657      * Prepares the payload for the request
658      *
659      * @param payload The String representation of the payload
660      * @return The Register representation of the payload
661      */
662     private ModbusRegister[] preparePayload(String payload) {
663
664         // determine number of registers
665         int l = (payload.length() + 1) / 2; // +1 because we need to include at least one termination symbol 0x00
666         if ((payload.length() + 1) % 2 != 0) {
667             l++;
668         }
669
670         ModbusRegister reg[] = new ModbusRegister[l];
671         byte[] b = payload.getBytes();
672         int ch = 0;
673         for (int i = 0; i < reg.length; i++) {
674             byte b1 = ch < b.length ? b[ch] : (byte) 0x00; // terminate with 0x00 if at the end of the payload
675             ch++;
676             byte b2 = ch < b.length ? b[ch] : (byte) 0x00;
677             ch++;
678             reg[i] = new ModbusRegister(b1, b2);
679         }
680         return reg;
681     }
682
683     /**
684      * Decodes the Helios device' response and updates the channel with the actual value of the variable
685      *
686      * @param response The registers received from the Helios device
687      * @return The value or <tt>null</tt> if an error occurred
688      */
689     private void processResponse(HeliosVariable v, ModbusRegisterArray registers) {
690         String r = ModbusBitUtilities
691                 .extractStringFromRegisters(registers, 0, registers.size() * 2, StandardCharsets.US_ASCII).toString();
692         String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string
693         // making sure we have a proper response and the response matches the requested variable
694         if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) {
695             if (this.isProperty(v.getName())) {
696                 try {
697                     updateProperty(v.getName(), v.formatPropertyValue(parts[1]));
698                 } catch (HeliosException e) {
699                     logger.warn("{} encountered Exception when trying to update property: {}",
700                             HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
701                 }
702             } else {
703                 this.updateState(v, parts[1]);
704             }
705         } else { // another variable was read
706             logger.warn("{} tried to read value from variable {} and the result provided by the device was {}",
707                     HeliosEasyControlsHandler.class.getSimpleName(), v.getName(), r);
708         }
709     }
710
711     private void updateState(HeliosVariable v, String value) {
712         String variableType = v.getType();
713         // System date and time
714         if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE)) {
715             this.updateSysDate(this.toDateTime(value));
716         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME)) {
717             this.updateSysTime(this.toDateTime(value));
718         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT)) {
719             this.updateUtcOffset(Integer.parseInt(value));
720             // Bypass
721         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY)) {
722             this.updateBypass(true, false, Integer.parseInt(value));
723         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH)) {
724             this.updateBypass(true, true, Integer.parseInt(value));
725         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY)) {
726             this.updateBypass(false, false, Integer.parseInt(value));
727         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH)) {
728             this.updateBypass(false, true, Integer.parseInt(value));
729         } else {
730             Channel channel = getThing().getChannel(v.getGroupAndName());
731             String itemType;
732             if (channel != null) {
733                 itemType = channel.getAcceptedItemType();
734                 if (itemType != null) {
735                     if (itemType.startsWith("Number:")) {
736                         itemType = "Number";
737                     }
738                     switch (itemType) {
739                         case "Number":
740                             if (((variableType.equals(HeliosVariable.TYPE_INTEGER))
741                                     || (variableType == HeliosVariable.TYPE_FLOAT)) && (!value.equals("-"))) {
742                                 State state = null;
743                                 if (v.getUnit() == null) {
744                                     state = DecimalType.valueOf(value);
745                                 } else { // QuantityType
746                                     state = this.toQuantityType(value, v.getUnit());
747                                 }
748                                 if (state != null) {
749                                     updateState(v.getGroupAndName(), state);
750                                     updateStatus(ThingStatus.ONLINE);
751                                     // update date format and UTC offset upon read
752                                     if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE_FORMAT)) {
753                                         this.dateFormat = Integer.parseInt(value);
754                                     }
755                                 }
756                             }
757                             break;
758                         case "Switch":
759                             if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
760                                 updateState(v.getGroupAndName(), value.equals("1") ? OnOffType.ON : OnOffType.OFF);
761                             }
762                             break;
763                         case "String":
764                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
765                                 updateState(v.getGroupAndName(), StringType.valueOf(value));
766                             }
767                             break;
768                         case "DateTime":
769                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
770                                 updateState(v.getGroupAndName(), toDateTime(value));
771                             }
772                             break;
773                     }
774                 } else { // itemType was null
775                     logger.warn("{} couldn't determine item type of variable {}",
776                             HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
777                 }
778             } else { // channel was null
779                 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
780                         v.getName());
781             }
782         }
783     }
784
785     /**
786      * Logs an error (as a warning entry) and updates the thing status
787      *
788      * @param errorMsg The error message to be logged and provided with the Thing's status update
789      * @param status The Thing's new status
790      */
791     private void handleError(String errorMsg, ThingStatusDetail status) {
792         updateStatus(ThingStatus.OFFLINE, status, errorMsg);
793     }
794 }