2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.modbus.helioseasycontrols.internal;
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;
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;
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;
63 import com.google.gson.Gson;
64 import com.google.gson.reflect.TypeToken;
67 * The {@link HeliosEasyControlsHandler} is responsible for handling commands, which are
68 * sent to one of the channels.
70 * @author Bernhard Bauer - Initial contribution
73 public class HeliosEasyControlsHandler extends BaseThingHandler {
75 private final Logger logger = LoggerFactory.getLogger(HeliosEasyControlsHandler.class);
77 private @Nullable HeliosEasyControlsConfiguration config;
79 private @Nullable ScheduledFuture<?> pollingJob;
81 private @Nullable Map<String, HeliosVariable> variableMap;
84 * This flag is used to ensure read requests (consisting of a write and subsequent read) are not influenced by
87 private final Map<ModbusSlaveEndpoint, Semaphore> transactionLocks = new ConcurrentHashMap<>();
89 private final Gson gson = new Gson();
91 private @Nullable ModbusCommunicationInterface comms;
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
97 private class BypassDate {
98 private final int[] MONTH_MAX_DAYS = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
100 // initialization to avoid issues when updating before all variables were read
101 private int month = 1;
104 public BypassDate() {
107 public BypassDate(int day, int month) {
109 this.setMonth(month);
112 public void setMonth(int month) {
115 } else if (month > 12) {
122 public int getMonth() {
126 public void setDay(int day) {
130 this.day = Math.min(day, MONTH_MAX_DAYS[month - 1]);
134 public int getDay() {
138 public DateTimeType toDateTimeType() {
139 return new DateTimeType(ZonedDateTime.of(1900, this.month, this.day, 0, 0, 0, 0, ZoneId.of("UTC+00:00")));
143 private @Nullable BypassDate bypassFrom, bypassTo;
145 public HeliosEasyControlsHandler(Thing thing) {
150 * Reads variable definitions from JSON file and store them in variableMap
152 private void readVariableDefinition() {
153 Type vMapType = new TypeToken<Map<String, HeliosVariable>>() {
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);
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);
173 this.handleError("Variables definition file not found or of illegal format",
174 ThingStatusDetail.CONFIGURATION_ERROR);
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
182 * @return the endpoint handler or null if the bridge does not exist
184 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
185 Bridge bridge = getBridge();
186 if (bridge == null) {
187 logger.debug("Bridge is null");
190 if (bridge.getStatus() != ThingStatus.ONLINE) {
191 logger.debug("Bridge is not online");
195 ThingHandler handler = bridge.getHandler();
196 if (handler == null) {
197 logger.debug("Bridge handler is null");
201 if (handler instanceof ModbusEndpointThingHandler) {
202 return (ModbusEndpointThingHandler) handler;
204 logger.debug("Unexpected bridge handler: {}", handler);
210 * Get a reference to the modbus endpoint
212 private void connectEndpoint() {
213 if (this.comms != null) {
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);
227 comms = slaveEndpointThingHandler.getCommunicationInterface();
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);
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);
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)
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());
267 handleError("Variable definition is null", ThingStatusDetail.CONFIGURATION_ERROR);
269 }, config.getRefreshInterval(), config.getRefreshInterval(), TimeUnit.MILLISECONDS);
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);
276 if (this.variableMap == null) {
277 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
279 if (this.config == null) {
280 this.handleError("Binding configuration is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
286 public void dispose() {
287 if (this.pollingJob != null) {
288 this.pollingJob.cancel(true);
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));
307 scheduler.submit(() -> readValue(channelId));
309 } else { // write command
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)) {
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());
322 value = formatDate(channelId, ((DateTimeType) command).getZonedDateTime());
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;
334 case HeliosVariable.UNIT_DAY:
335 val = val.toUnit(Units.DAY);
337 case HeliosVariable.UNIT_HOUR:
338 val = val.toUnit(Units.HOUR);
340 case HeliosVariable.UNIT_MIN:
341 val = val.toUnit(Units.MINUTE);
343 case HeliosVariable.UNIT_SEC:
344 val = val.toUnit(Units.SECOND);
346 case HeliosVariable.UNIT_VOLT:
347 val = val.toUnit(Units.VOLT);
349 case HeliosVariable.UNIT_PERCENT:
350 val = val.toUnit(Units.PERCENT);
352 case HeliosVariable.UNIT_PPM:
353 val = val.toUnit(Units.PARTS_PER_MILLION);
355 case HeliosVariable.UNIT_TEMP:
356 val = val.toUnit(SIUnits.CELSIUS);
359 value = val != null ? String.valueOf(val.doubleValue()) : null; // ignore the UoM
364 final String v = value;
365 scheduler.submit(() -> {
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);
375 } catch (HeliosException e) {
376 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
377 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
385 public Collection<Class<? extends ThingHandlerService>> getServices() {
386 return Collections.singleton(HeliosEasyControlsActions.class);
390 * Checks if the provided variable name is a property
392 * @param variableName The variable's name
393 * @return true if the variable is a property
395 private boolean isProperty(String variableName) {
396 return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
400 * Writes a variable value to the Helios device
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
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);
412 Map<String, HeliosVariable> variableMap = this.variableMap;
413 if (variableMap != null) {
414 HeliosVariable v = variableMap.get(variableName);
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) {
423 String payload = v.getVariableString() + "=" + value;
424 ModbusCommunicationInterface comms = this.comms;
426 final Semaphore lock = transactionLocks.get(comms.getEndpoint());
429 comms.submitOneTimeWrite(
430 new ModbusWriteRegisterRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
431 HeliosEasyControlsBindingConstants.START_ADDRESS, preparePayload(payload),
432 true, HeliosEasyControlsBindingConstants.MAX_TRIES),
435 updateStatus(ThingStatus.ONLINE);
438 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
439 "Error writing to device: " + failureInfo.getCause().getMessage());
441 } catch (InterruptedException e) {
443 "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
444 HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
447 } else { // comms is null
448 this.handleError("Modbus communication interface is null", ThingStatusDetail.COMMUNICATION_ERROR);
455 * Read a variable from the Helios device
457 * @param variableName The variable name
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()) {
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());
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),
486 Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
487 if (registers.isPresent()) {
488 processResponse(v, registers.get());
492 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
493 "Error reading from device: " + failureInfo.getCause().getMessage());
497 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
498 "Error writing to device: " + failureInfo.getCause().getMessage());
504 if (this.comms == null) {
505 this.handleError("Modbus communication interface is unavailable",
506 ThingStatusDetail.COMMUNICATION_ERROR);
508 if (variableMap == null) {
509 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
514 private void updateSysDate(DateTimeType dateTime) {
515 this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
518 private void updateSysTime(DateTimeType dateTime) {
519 this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
522 private void updateUtcOffset(int utcOffset) {
523 this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
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;
539 private void setSysDateTime(ZonedDateTime date) {
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());
553 protected void setSysDateTime() {
554 this.setSysDateTime(ZonedDateTime.now());
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();
563 bypassDate.setMonth(val);
566 bypassDate.setDay(val);
568 updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
569 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
571 this.bypassFrom = bypassDate;
574 this.bypassTo = bypassDate;
578 protected void setBypass(boolean from, int day, int month) {
579 BypassDate bypassDate = new BypassDate(day, month);
581 this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
582 : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(bypassDate.getDay()));
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());
594 * Formats the provided date to a string in the device's configured date format
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
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) {
606 String d = Integer.toString(date.getDayOfMonth());
607 if (d.length() == 1) {
610 if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
611 return d + "." + m + "." + y;
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;
621 return d + "." + m + "." + y;
627 * Returns a DateTimeType object based on the provided String and the device's configured date format
629 * @param date The date string read from the device
630 * @return A DateTimeType object representing the date or time specified
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];
643 case 1: // mm.dd.yyyy
644 dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
646 case 2: // yyyy.mm.dd
647 dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
650 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
653 return DateTimeType.valueOf(dateTime);
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);
660 private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
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);
685 * Prepares the payload for the request
687 * @param payload The String representation of the payload
688 * @return The Register representation of the payload
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
695 + ((asciiBytes.length % 2 == 0) ? 1 : 0); // to have even number of bytes
696 assert bufferLength % 2 == 0; // Invariant, ensured above
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++) {
704 return new ModbusRegisterArray(buffer);
708 * Decodes the Helios device' response and updates the channel with the actual value of the variable
710 * @param response The registers received from the Helios device
711 * @return The value or <tt>null</tt> if an error occurred
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())) {
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());
727 this.updateState(v, parts[1]);
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);
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));
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));
754 Channel channel = getThing().getChannel(v.getGroupAndName());
756 if (channel != null) {
757 itemType = channel.getAcceptedItemType();
758 if (itemType != null) {
759 if (itemType.startsWith("Number:")) {
764 if (((variableType.equals(HeliosVariable.TYPE_INTEGER))
765 || (variableType == HeliosVariable.TYPE_FLOAT)) && (!value.equals("-"))) {
767 if (v.getUnit() == null) {
768 state = DecimalType.valueOf(value);
769 } else { // QuantityType
770 state = this.toQuantityType(value, v.getUnit());
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);
783 if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
784 updateState(v.getGroupAndName(), value.equals("1") ? OnOffType.ON : OnOffType.OFF);
788 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
789 updateState(v.getGroupAndName(), StringType.valueOf(value));
793 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
794 updateState(v.getGroupAndName(), toDateTime(value));
798 } else { // itemType was null
799 logger.warn("{} couldn't determine item type of variable {}",
800 HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
802 } else { // channel was null
803 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
810 * Logs an error (as a warning entry) and updates the thing status
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
815 private void handleError(String errorMsg, ThingStatusDetail status) {
816 updateStatus(ThingStatus.OFFLINE, status, errorMsg);