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