]> git.basschouten.com Git - openhab-addons.git/blob
d0a8f366b60f9665b8cecfea358674fc7a34e55b
[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.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.Set;
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 thingHandler) {
211             return thingHandler;
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 dateTimeCommand) {
323                 try {
324                     ZonedDateTime d = dateTimeCommand.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, dateTimeCommand.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<?> val) {
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                         if (unit != null) {
349                             switch (unit) {
350                                 case HeliosVariable.UNIT_DAY:
351                                     val = val.toUnit(Units.DAY);
352                                     break;
353                                 case HeliosVariable.UNIT_HOUR:
354                                     val = val.toUnit(Units.HOUR);
355                                     break;
356                                 case HeliosVariable.UNIT_MIN:
357                                     val = val.toUnit(Units.MINUTE);
358                                     break;
359                                 case HeliosVariable.UNIT_SEC:
360                                     val = val.toUnit(Units.SECOND);
361                                     break;
362                                 case HeliosVariable.UNIT_VOLT:
363                                     val = val.toUnit(Units.VOLT);
364                                     break;
365                                 case HeliosVariable.UNIT_PERCENT:
366                                     val = val.toUnit(Units.PERCENT);
367                                     break;
368                                 case HeliosVariable.UNIT_PPM:
369                                     val = val.toUnit(Units.PARTS_PER_MILLION);
370                                     break;
371                                 case HeliosVariable.UNIT_TEMP:
372                                     val = val.toUnit(SIUnits.CELSIUS);
373                                     break;
374                             }
375                             value = val != null ? String.valueOf(val.doubleValue()) : null; // ignore the UoM
376                         }
377                     }
378                 }
379             }
380             if (value != null) {
381                 final String v = value;
382                 scheduler.submit(() -> {
383                     try {
384                         writeValue(channelId, v);
385                         if (variableMap != null) {
386                             HeliosVariable variable = variableMap.get(channelId);
387                             if (variable != null) {
388                                 updateState(variable, v);
389                                 updateStatus(ThingStatus.ONLINE);
390                             }
391                         }
392                     } catch (HeliosException e) {
393                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
394                                 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
395                     } catch (InterruptedException e) {
396                         logger.debug(
397                                 "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
398                                 HeliosEasyControlsHandler.class.getSimpleName(), channelId, e.getMessage());
399
400                     }
401                 });
402             }
403         }
404     }
405
406     @Override
407     public Collection<Class<? extends ThingHandlerService>> getServices() {
408         return Set.of(HeliosEasyControlsActions.class);
409     }
410
411     /**
412      * Checks if the provided variable name is a property
413      *
414      * @param variableName The variable's name
415      * @return true if the variable is a property
416      */
417     private boolean isProperty(String variableName) {
418         return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
419     }
420
421     /**
422      * Writes a variable value to the Helios device
423      *
424      * @param variableName The variable name
425      * @param value The new value
426      * @return The value if the transaction succeeded, <tt>null</tt> otherwise
427      * @throws HeliosException Thrown if the variable is read-only or the provided value is out of range
428      */
429     public void writeValue(String variableName, String value) throws HeliosException, InterruptedException {
430         if (this.variableMap == null) {
431             this.handleError("Variable definition is unavailable.", ThingStatusDetail.CONFIGURATION_ERROR);
432             return;
433         } else {
434             Map<String, HeliosVariable> variableMap = this.variableMap;
435             if (variableMap != null) {
436                 HeliosVariable v = variableMap.get(variableName);
437
438                 if (v != null) {
439                     if (!v.hasWriteAccess()) {
440                         throw new HeliosException("Variable " + variableName + " is read-only");
441                     } else if (!v.isInAllowedRange(value)) {
442                         throw new HeliosException(
443                                 "Value " + value + " is outside of allowed range of variable " + variableName);
444                     } else if (this.comms != null) {
445                         // write to device
446                         String payload = v.getVariableString() + "=" + value;
447                         ModbusCommunicationInterface comms = this.comms;
448                         if (comms != null) {
449                             final Semaphore lock = transactionLocks.get(comms.getEndpoint());
450                             if (lock != null) {
451                                 lock.acquire();
452                                 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
453                                         HeliosEasyControlsBindingConstants.UNIT_ID,
454                                         HeliosEasyControlsBindingConstants.START_ADDRESS, preparePayload(payload), true,
455                                         HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
456                                             lock.release();
457                                             updateStatus(ThingStatus.ONLINE);
458                                         }, failureInfo -> {
459                                             lock.release();
460                                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
461                                                     "Error writing to device: " + failureInfo.getCause().getMessage());
462                                         });
463                             }
464                         }
465                     } else { // comms is null
466                         this.handleError("Modbus communication interface is null",
467                                 ThingStatusDetail.COMMUNICATION_ERROR);
468                     }
469                 }
470             }
471         }
472     }
473
474     /**
475      * Read a variable from the Helios device
476      *
477      * @param variableName The variable name
478      * @return The value
479      */
480     public void readValue(String variableName) {
481         Map<String, HeliosVariable> variableMap = this.variableMap;
482         ModbusCommunicationInterface comms = this.comms;
483         if ((comms != null) && (variableMap != null)) {
484             final Semaphore lock = transactionLocks.get(comms.getEndpoint());
485             HeliosVariable v = variableMap.get(variableName);
486             if ((v != null) && v.hasReadAccess() && (lock != null)) {
487                 try {
488                     lock.acquire(); // will block until lock is available
489                 } catch (InterruptedException e) {
490                     logger.warn("{} encountered Exception when trying to read variable {} from the device: {}",
491                             HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
492                     return;
493                 }
494                 // write variable name to register
495                 String payload = v.getVariableString();
496                 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
497                         HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS,
498                         preparePayload(payload), true, HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
499                             comms.submitOneTimePoll(
500                                     new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
501                                             ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
502                                             HeliosEasyControlsBindingConstants.START_ADDRESS, v.getCount(),
503                                             HeliosEasyControlsBindingConstants.MAX_TRIES),
504                                     pollResult -> {
505                                         lock.release();
506                                         Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
507                                         if (registers.isPresent()) {
508                                             processResponse(v, registers.get());
509                                         }
510                                     }, failureInfo -> {
511                                         lock.release();
512                                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
513                                                 "Error reading from device: " + failureInfo.getCause().getMessage());
514                                     });
515                         }, failureInfo -> {
516                             lock.release();
517                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
518                                     "Error writing to device: " + failureInfo.getCause().getMessage());
519
520                         });
521             }
522
523         } else {
524             if (this.comms == null) {
525                 this.handleError("Modbus communication interface is unavailable",
526                         ThingStatusDetail.COMMUNICATION_ERROR);
527             }
528             if (variableMap == null) {
529                 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
530             }
531         }
532     }
533
534     private void updateSysDate(DateTimeType dateTime) {
535         this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
536     }
537
538     private void updateSysTime(DateTimeType dateTime) {
539         this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
540     }
541
542     private void updateUtcOffset(int utcOffset) {
543         this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
544     }
545
546     private void updateSysDateTime(ZonedDateTime dateTime, boolean updateDate, int utcOffset) {
547         ZonedDateTime sysDate = this.sysDate;
548         sysDate = ZonedDateTime.of(updateDate ? dateTime.getYear() : sysDate.getYear(),
549                 updateDate ? dateTime.getMonthValue() : sysDate.getMonthValue(),
550                 updateDate ? dateTime.getDayOfMonth() : sysDate.getDayOfMonth(),
551                 updateDate ? sysDate.getHour() : dateTime.getHour(),
552                 updateDate ? sysDate.getMinute() : dateTime.getMinute(),
553                 updateDate ? sysDate.getSecond() : dateTime.getSecond(), 0,
554                 ZoneId.of("UTC" + (utcOffset >= 0 ? "+" : "") + String.format("%02d", utcOffset) + ":00"));
555         updateState("general#" + HeliosEasyControlsBindingConstants.SYS_DATE, new DateTimeType(sysDate));
556         this.sysDate = sysDate;
557     }
558
559     private void setSysDateTime(ZonedDateTime date) throws InterruptedException {
560         try {
561             this.writeValue(HeliosEasyControlsBindingConstants.DATE,
562                     this.formatDate(HeliosEasyControlsBindingConstants.DATE, date));
563             this.writeValue(HeliosEasyControlsBindingConstants.TIME,
564                     date.getHour() + ":" + date.getMinute() + ":" + date.getSecond());
565             this.writeValue(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT,
566                     Integer.toString(date.getOffset().getTotalSeconds() / 60 / 60));
567         } catch (HeliosException e) {
568             logger.warn("{} encountered Exception when trying to set system date: {}",
569                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
570         }
571     }
572
573     protected void setSysDateTime() throws InterruptedException {
574         this.setSysDateTime(ZonedDateTime.now());
575     }
576
577     private void updateBypass(boolean from, boolean month, int val) {
578         BypassDate bypassDate = from ? this.bypassFrom : this.bypassTo;
579         if (bypassDate == null) {
580             bypassDate = new BypassDate();
581         }
582         if (month) {
583             bypassDate.setMonth(val);
584
585         } else {
586             bypassDate.setDay(val);
587         }
588         updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
589                 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
590         if (from) {
591             this.bypassFrom = bypassDate;
592
593         } else {
594             this.bypassTo = bypassDate;
595         }
596     }
597
598     protected void setBypass(boolean from, int day, int month) throws InterruptedException {
599         BypassDate bypassDate = new BypassDate(day, month);
600         try {
601             this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
602                     : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(bypassDate.getDay()));
603             this.writeValue(
604                     from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH
605                             : HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH,
606                     Integer.toString(bypassDate.getMonth()));
607         } catch (HeliosException e) {
608             logger.warn("{} encountered Exception when trying to set bypass period: {}",
609                     HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
610         }
611     }
612
613     /**
614      * Formats the provided date to a string in the device's configured date format
615      *
616      * @param variableName the variable name
617      * @param date the date to be formatted
618      * @return a string in the device's configured date format
619      */
620     public String formatDate(String variableName, ZonedDateTime date) {
621         String y = Integer.toString(date.getYear());
622         String m = Integer.toString(date.getMonthValue());
623         if (m.length() == 1) {
624             m = "0" + m;
625         }
626         String d = Integer.toString(date.getDayOfMonth());
627         if (d.length() == 1) {
628             d = "0" + d;
629         }
630         if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
631             return d + "." + m + "." + y;
632         } else {
633             switch (this.dateFormat) {
634                 case 0: // dd.mm.yyyy
635                     return d + "." + m + "." + y;
636                 case 1: // mm.dd.yyyy
637                     return m + "." + d + "." + y;
638                 case 2: // yyyy.mm.dd
639                     return y + "." + m + "." + d;
640                 default:
641                     return d + "." + m + "." + y;
642             }
643         }
644     }
645
646     private List<String> getMessages(long bitMask, int bits, String prefix) {
647         ArrayList<String> msg = new ArrayList<String>();
648         long mask = 1;
649         for (int i = 0; i < bits; i++) {
650             if ((bitMask & mask) != 0) {
651                 msg.add(translationProvider.getText(prefix + i));
652             }
653             mask <<= 1;
654         }
655         return msg;
656     }
657
658     /**
659      * Transforms the errors provided by the device into a human readable form (the basis for the
660      * corresponding action)
661      *
662      * @return an <code>List</code> of messages indicated by the error flags sent by the device
663      */
664     protected List<String> getErrorMessages() {
665         return this.getMessages(this.errors, HeliosEasyControlsBindingConstants.BITS_ERROR_MSG,
666                 HeliosEasyControlsBindingConstants.PREFIX_ERROR_MSG);
667     }
668
669     /**
670      * Transforms the warnings provided by the device into a human readable form (the basis for the
671      * corresponding action)
672      *
673      * @return an <code>List</code> of messages indicated by the warning flags sent by the device
674      */
675     protected List<String> getWarningMessages() {
676         return this.getMessages(this.warnings, HeliosEasyControlsBindingConstants.BITS_WARNING_MSG,
677                 HeliosEasyControlsBindingConstants.PREFIX_WARNING_MSG);
678     }
679
680     /**
681      * Transforms the infos provided by the device into a human readable form (the basis for the
682      * corresponding action)
683      *
684      * @return an <code>List</code> of messages indicated by the info flags sent by the device
685      */
686     protected List<String> getInfoMessages() {
687         return this.getMessages(this.infos, HeliosEasyControlsBindingConstants.BITS_INFO_MSG,
688                 HeliosEasyControlsBindingConstants.PREFIX_INFO_MSG);
689     }
690
691     /**
692      * Transforms the status flags provided by the device into a human readable form (the basis for the
693      * corresponding action)
694      *
695      * @return an <code>List</code> of messages indicated by the status flags sent by the device
696      */
697     protected List<String> getStatusMessages() {
698         ArrayList<String> msg = new ArrayList<String>();
699         if (this.statusFlags.length() == HeliosEasyControlsBindingConstants.BITS_STATUS_MSG) {
700             for (int i = 0; i < HeliosEasyControlsBindingConstants.BITS_STATUS_MSG; i++) {
701                 String key = HeliosEasyControlsBindingConstants.PREFIX_STATUS_MSG + i + "."
702                         + (this.statusFlags.substring(HeliosEasyControlsBindingConstants.BITS_STATUS_MSG - i - 1,
703                                 HeliosEasyControlsBindingConstants.BITS_STATUS_MSG - i));
704                 String text = translationProvider.getText(key);
705                 if (!text.equals(key)) { // there is a text in the properties file (no text => flag is irrelevant)
706                     msg.add(text);
707                 }
708             }
709         } else {
710             msg.add("Status messages have not yet been read from the device");
711         }
712         return msg;
713     }
714
715     /**
716      * Returns a DateTimeType object based on the provided String and the device's configured date format
717      *
718      * @param date The date string read from the device
719      * @return A DateTimeType object representing the date or time specified
720      */
721     private DateTimeType toDateTime(String date) {
722         String[] dateTimeParts = null;
723         String dateTime = date;
724         dateTimeParts = date.split("\\."); // try to split date components
725         if (dateTimeParts.length == 1) { // time
726             return DateTimeType.valueOf(date);
727         } else if (dateTimeParts.length == 3) { // date - we'll try the device's date format
728             switch (this.dateFormat) {
729                 case 0: // dd.mm.yyyy
730                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
731                     break;
732                 case 1: // mm.dd.yyyy
733                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
734                     break;
735                 case 2: // yyyy.mm.dd
736                     dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
737                     break;
738                 default:
739                     dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
740                     break;
741             }
742             return DateTimeType.valueOf(dateTime);
743         }
744         // falling back to default date format (apparently using the configured format has failed)
745         dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
746         return DateTimeType.valueOf(dateTime);
747     }
748
749     private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
750         if (unit == null) {
751             return null;
752         } else if (unit.equals(HeliosVariable.UNIT_DAY)) {
753             return new QuantityType<>(Integer.parseInt(value), Units.DAY);
754         } else if (unit.equals(HeliosVariable.UNIT_HOUR)) {
755             return new QuantityType<>(Integer.parseInt(value), Units.HOUR);
756         } else if (unit.equals(HeliosVariable.UNIT_MIN)) {
757             return new QuantityType<>(Integer.parseInt(value), Units.MINUTE);
758         } else if (unit.equals(HeliosVariable.UNIT_SEC)) {
759             return new QuantityType<>(Integer.parseInt(value), Units.SECOND);
760         } else if (unit.equals(HeliosVariable.UNIT_VOLT)) {
761             return new QuantityType<>(Float.parseFloat(value), Units.VOLT);
762         } else if (unit.equals(HeliosVariable.UNIT_PERCENT)) {
763             return new QuantityType<>(Float.parseFloat(value), Units.PERCENT);
764         } else if (unit.equals(HeliosVariable.UNIT_PPM)) {
765             return new QuantityType<>(Float.parseFloat(value), Units.PARTS_PER_MILLION);
766         } else if (unit.equals(HeliosVariable.UNIT_TEMP)) {
767             return new QuantityType<>(Float.parseFloat(value), SIUnits.CELSIUS);
768         } else {
769             return null;
770         }
771     }
772
773     /**
774      * Prepares the payload for the request
775      *
776      * @param payload The String representation of the payload
777      * @return The Register representation of the payload
778      */
779     private static ModbusRegisterArray preparePayload(String payload) {
780         // determine number of registers
781         byte[] asciiBytes = payload.getBytes(StandardCharsets.US_ASCII);
782         int bufferLength = asciiBytes.length // ascii characters
783                 + 1 // NUL byte
784                 + ((asciiBytes.length % 2 == 0) ? 1 : 0); // to have even number of bytes
785         assert bufferLength % 2 == 0; // Invariant, ensured above
786
787         byte[] buffer = new byte[bufferLength];
788         System.arraycopy(asciiBytes, 0, buffer, 0, asciiBytes.length);
789         // Fill in rest of bytes with NUL bytes
790         for (int i = asciiBytes.length; i < buffer.length; i++) {
791             buffer[i] = '\0';
792         }
793         return new ModbusRegisterArray(buffer);
794     }
795
796     /**
797      * Decodes the Helios device' response and updates the channel with the actual value of the variable
798      *
799      * @param response The registers received from the Helios device
800      * @return The value or <tt>null</tt> if an error occurred
801      */
802     private void processResponse(HeliosVariable v, ModbusRegisterArray registers) {
803         String r = ModbusBitUtilities.extractStringFromRegisters(registers, 0, registers.size() * 2,
804                 StandardCharsets.US_ASCII);
805         String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string
806         // making sure we have a proper response and the response matches the requested variable
807         if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) {
808             if (this.isProperty(v.getName())) {
809                 try {
810                     updateProperty(
811                             translationProvider
812                                     .getText(HeliosEasyControlsBindingConstants.PROPERTIES_PREFIX + v.getName()),
813                             v.formatPropertyValue(parts[1], translationProvider));
814                 } catch (HeliosException e) {
815                     logger.warn("{} encountered Exception when trying to update property: {}",
816                             HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
817                 }
818             } else {
819                 this.updateState(v, parts[1]);
820             }
821         } else { // another variable was read
822             logger.warn("{} tried to read value from variable {} and the result provided by the device was {}",
823                     HeliosEasyControlsHandler.class.getSimpleName(), v.getName(), r);
824         }
825     }
826
827     private void updateState(HeliosVariable v, String value) {
828         String variableType = v.getType();
829         // System date and time
830         if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE)) {
831             this.updateSysDate(this.toDateTime(value));
832         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME)) {
833             this.updateSysTime(this.toDateTime(value));
834         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT)) {
835             this.updateUtcOffset(Integer.parseInt(value));
836             // Bypass
837         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY)) {
838             this.updateBypass(true, false, Integer.parseInt(value));
839         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH)) {
840             this.updateBypass(true, true, Integer.parseInt(value));
841         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY)) {
842             this.updateBypass(false, false, Integer.parseInt(value));
843         } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH)) {
844             this.updateBypass(false, true, Integer.parseInt(value));
845         } else {
846             Channel channel = getThing().getChannel(v.getGroupAndName());
847             String itemType;
848             if (channel != null) {
849                 itemType = channel.getAcceptedItemType();
850                 if (itemType != null) {
851                     if (itemType.startsWith("Number:")) {
852                         itemType = "Number";
853                     }
854                     switch (itemType) {
855                         case "Number":
856                             if (((HeliosVariable.TYPE_INTEGER.equals(variableType))
857                                     || (HeliosVariable.TYPE_FLOAT.equals(variableType))) && (!"-".equals(value))) {
858                                 State state = null;
859                                 if (v.getUnit() == null) {
860                                     state = DecimalType.valueOf(value);
861                                 } else { // QuantityType
862                                     state = this.toQuantityType(value, v.getUnit());
863                                 }
864                                 if (state != null) {
865                                     updateState(v.getGroupAndName(), state);
866                                     updateStatus(ThingStatus.ONLINE);
867                                     // update date format and messages upon read
868                                     if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE_FORMAT)) {
869                                         this.dateFormat = Integer.parseInt(value);
870                                     } else if (v.getName().equals(HeliosEasyControlsBindingConstants.ERRORS)) {
871                                         this.errors = Long.parseLong(value);
872                                     } else if (v.getName().equals(HeliosEasyControlsBindingConstants.WARNINGS)) {
873                                         this.warnings = Integer.parseInt(value);
874                                     } else if (v.getName().equals(HeliosEasyControlsBindingConstants.INFOS)) {
875                                         this.infos = Integer.parseInt(value);
876                                     }
877                                 }
878                             }
879                             break;
880                         case "Switch":
881                             if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
882                                 updateState(v.getGroupAndName(), "1".equals(value) ? OnOffType.ON : OnOffType.OFF);
883                             }
884                             break;
885                         case "String":
886                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
887                                 updateState(v.getGroupAndName(), StringType.valueOf(value));
888                                 if (v.getName().equals(HeliosEasyControlsBindingConstants.STATUS_FLAGS)) {
889                                     this.statusFlags = value;
890                                 }
891                             }
892                             break;
893                         case "DateTime":
894                             if (variableType.equals(HeliosVariable.TYPE_STRING)) {
895                                 updateState(v.getGroupAndName(), toDateTime(value));
896                             }
897                             break;
898                     }
899                 } else { // itemType was null
900                     logger.warn("{} couldn't determine item type of variable {}",
901                             HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
902                 }
903             } else { // channel was null
904                 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
905                         v.getName());
906             }
907         }
908     }
909
910     /**
911      * Logs an error (as a warning entry) and updates the thing status
912      *
913      * @param errorMsg The error message to be logged and provided with the Thing's status update
914      * @param status The Thing's new status
915      */
916     private void handleError(String errorMsg, ThingStatusDetail status) {
917         updateStatus(ThingStatus.OFFLINE, status, errorMsg);
918     }
919 }