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