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