2 * Copyright (c) 2010-2023 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.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
27 import java.util.Optional;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.Semaphore;
31 import java.util.concurrent.TimeUnit;
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;
65 import com.google.gson.Gson;
66 import com.google.gson.reflect.TypeToken;
69 * The {@link HeliosEasyControlsHandler} is responsible for handling commands, which are
70 * sent to one of the channels.
72 * @author Bernhard Bauer - Initial contribution
75 public class HeliosEasyControlsHandler extends BaseThingHandler {
77 private final Logger logger = LoggerFactory.getLogger(HeliosEasyControlsHandler.class);
79 private final HeliosEasyControlsTranslationProvider translationProvider;
81 private @Nullable HeliosEasyControlsConfiguration config;
83 private @Nullable ScheduledFuture<?> pollingJob;
85 private @Nullable Map<String, HeliosVariable> variableMap;
88 * This flag is used to ensure read requests (consisting of a write and subsequent read) are not influenced by
91 private final Map<ModbusSlaveEndpoint, Semaphore> transactionLocks = new ConcurrentHashMap<>();
93 private final Gson gson = new Gson();
95 private @Nullable ModbusCommunicationInterface comms;
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 = "";
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 };
108 // initialization to avoid issues when updating before all variables were read
109 private int month = 1;
112 public BypassDate() {
115 public BypassDate(int day, int month) {
117 this.setMonth(month);
120 public void setMonth(int month) {
123 } else if (month > 12) {
130 public int getMonth() {
134 public void setDay(int day) {
138 this.day = Math.min(day, MONTH_MAX_DAYS[month - 1]);
142 public int getDay() {
146 public DateTimeType toDateTimeType() {
147 return new DateTimeType(ZonedDateTime.of(1900, this.month, this.day, 0, 0, 0, 0, ZoneId.of("UTC+00:00")));
151 private @Nullable BypassDate bypassFrom, bypassTo;
153 public HeliosEasyControlsHandler(Thing thing, HeliosEasyControlsTranslationProvider translationProvider) {
155 this.translationProvider = translationProvider;
159 * Reads variable definitions from JSON file and store them in variableMap
161 private void readVariableDefinition() {
162 Type vMapType = new TypeToken<Map<String, HeliosVariable>>() {
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);
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);
182 this.handleError("Variables definition file not found or of illegal format",
183 ThingStatusDetail.CONFIGURATION_ERROR);
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
191 * @return the endpoint handler or null if the bridge does not exist
193 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
194 Bridge bridge = getBridge();
195 if (bridge == null) {
196 logger.debug("Bridge is null");
199 if (bridge.getStatus() != ThingStatus.ONLINE) {
200 logger.debug("Bridge is not online");
204 ThingHandler handler = bridge.getHandler();
205 if (handler == null) {
206 logger.debug("Bridge handler is null");
210 if (handler instanceof ModbusEndpointThingHandler) {
211 return (ModbusEndpointThingHandler) handler;
213 logger.debug("Unexpected bridge handler: {}", handler);
219 * Get a reference to the modbus endpoint
221 private void connectEndpoint() {
222 if (this.comms != null) {
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);
236 comms = slaveEndpointThingHandler.getCommunicationInterface();
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);
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);
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)
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());
276 handleError("Variable definition is null", ThingStatusDetail.CONFIGURATION_ERROR);
278 }, config.getRefreshInterval(), config.getRefreshInterval(), TimeUnit.MILLISECONDS);
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);
285 if (this.variableMap == null) {
286 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
288 if (this.config == null) {
289 this.handleError("Binding configuration is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
295 public void dispose() {
296 if (this.pollingJob != null) {
297 this.pollingJob.cancel(true);
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));
316 scheduler.submit(() -> readValue(channelId));
318 } else { // write command
320 if (command instanceof OnOffType) {
321 value = command == OnOffType.ON ? "1" : "0";
322 } else if (command instanceof DateTimeType) {
324 ZonedDateTime d = ((DateTimeType) command).getZonedDateTime();
325 if (channelId.equals(HeliosEasyControlsBindingConstants.SYS_DATE)) {
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());
332 value = formatDate(channelId, ((DateTimeType) command).getZonedDateTime());
334 } catch (InterruptedException e) {
336 "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
337 HeliosEasyControlsHandler.class.getSimpleName(), channelId, e.getMessage());
339 } else if ((command instanceof DecimalType) || (command instanceof StringType)) {
340 value = command.toString();
341 } else if (command instanceof QuantityType<?>) {
342 // convert item's unit to the Helios device's unit
343 Map<String, HeliosVariable> variableMap = this.variableMap;
344 if (variableMap != null) {
345 HeliosVariable v = variableMap.get(channelId);
347 String unit = v.getUnit();
348 QuantityType<?> val = (QuantityType<?>) command;
351 case HeliosVariable.UNIT_DAY:
352 val = val.toUnit(Units.DAY);
354 case HeliosVariable.UNIT_HOUR:
355 val = val.toUnit(Units.HOUR);
357 case HeliosVariable.UNIT_MIN:
358 val = val.toUnit(Units.MINUTE);
360 case HeliosVariable.UNIT_SEC:
361 val = val.toUnit(Units.SECOND);
363 case HeliosVariable.UNIT_VOLT:
364 val = val.toUnit(Units.VOLT);
366 case HeliosVariable.UNIT_PERCENT:
367 val = val.toUnit(Units.PERCENT);
369 case HeliosVariable.UNIT_PPM:
370 val = val.toUnit(Units.PARTS_PER_MILLION);
372 case HeliosVariable.UNIT_TEMP:
373 val = val.toUnit(SIUnits.CELSIUS);
376 value = val != null ? String.valueOf(val.doubleValue()) : null; // ignore the UoM
382 final String v = value;
383 scheduler.submit(() -> {
385 writeValue(channelId, v);
386 if (variableMap != null) {
387 HeliosVariable variable = variableMap.get(channelId);
388 if (variable != null) {
389 updateState(variable, v);
390 updateStatus(ThingStatus.ONLINE);
393 } catch (HeliosException e) {
394 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
395 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
396 } catch (InterruptedException e) {
398 "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
399 HeliosEasyControlsHandler.class.getSimpleName(), channelId, e.getMessage());
408 public Collection<Class<? extends ThingHandlerService>> getServices() {
409 return Collections.singleton(HeliosEasyControlsActions.class);
413 * Checks if the provided variable name is a property
415 * @param variableName The variable's name
416 * @return true if the variable is a property
418 private boolean isProperty(String variableName) {
419 return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
423 * Writes a variable value to the Helios device
425 * @param variableName The variable name
426 * @param value The new value
427 * @return The value if the transaction succeeded, <tt>null</tt> otherwise
428 * @throws HeliosException Thrown if the variable is read-only or the provided value is out of range
430 public void writeValue(String variableName, String value) throws HeliosException, InterruptedException {
431 if (this.variableMap == null) {
432 this.handleError("Variable definition is unavailable.", ThingStatusDetail.CONFIGURATION_ERROR);
435 Map<String, HeliosVariable> variableMap = this.variableMap;
436 if (variableMap != null) {
437 HeliosVariable v = variableMap.get(variableName);
440 if (!v.hasWriteAccess()) {
441 throw new HeliosException("Variable " + variableName + " is read-only");
442 } else if (!v.isInAllowedRange(value)) {
443 throw new HeliosException(
444 "Value " + value + " is outside of allowed range of variable " + variableName);
445 } else if (this.comms != null) {
447 String payload = v.getVariableString() + "=" + value;
448 ModbusCommunicationInterface comms = this.comms;
450 final Semaphore lock = transactionLocks.get(comms.getEndpoint());
453 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
454 HeliosEasyControlsBindingConstants.UNIT_ID,
455 HeliosEasyControlsBindingConstants.START_ADDRESS, preparePayload(payload), true,
456 HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
458 updateStatus(ThingStatus.ONLINE);
461 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
462 "Error writing to device: " + failureInfo.getCause().getMessage());
466 } else { // comms is null
467 this.handleError("Modbus communication interface is null",
468 ThingStatusDetail.COMMUNICATION_ERROR);
476 * Read a variable from the Helios device
478 * @param variableName The variable name
481 public void readValue(String variableName) {
482 Map<String, HeliosVariable> variableMap = this.variableMap;
483 ModbusCommunicationInterface comms = this.comms;
484 if ((comms != null) && (variableMap != null)) {
485 final Semaphore lock = transactionLocks.get(comms.getEndpoint());
486 HeliosVariable v = variableMap.get(variableName);
487 if ((v != null) && v.hasReadAccess() && (lock != null)) {
489 lock.acquire(); // will block until lock is available
490 } catch (InterruptedException e) {
491 logger.warn("{} encountered Exception when trying to read variable {} from the device: {}",
492 HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
495 // write variable name to register
496 String payload = v.getVariableString();
497 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
498 HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS,
499 preparePayload(payload), true, HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
500 comms.submitOneTimePoll(
501 new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
502 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
503 HeliosEasyControlsBindingConstants.START_ADDRESS, v.getCount(),
504 HeliosEasyControlsBindingConstants.MAX_TRIES),
507 Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
508 if (registers.isPresent()) {
509 processResponse(v, registers.get());
513 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
514 "Error reading from device: " + failureInfo.getCause().getMessage());
518 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
519 "Error writing to device: " + failureInfo.getCause().getMessage());
525 if (this.comms == null) {
526 this.handleError("Modbus communication interface is unavailable",
527 ThingStatusDetail.COMMUNICATION_ERROR);
529 if (variableMap == null) {
530 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
535 private void updateSysDate(DateTimeType dateTime) {
536 this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
539 private void updateSysTime(DateTimeType dateTime) {
540 this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
543 private void updateUtcOffset(int utcOffset) {
544 this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
547 private void updateSysDateTime(ZonedDateTime dateTime, boolean updateDate, int utcOffset) {
548 ZonedDateTime sysDate = this.sysDate;
549 sysDate = ZonedDateTime.of(updateDate ? dateTime.getYear() : sysDate.getYear(),
550 updateDate ? dateTime.getMonthValue() : sysDate.getMonthValue(),
551 updateDate ? dateTime.getDayOfMonth() : sysDate.getDayOfMonth(),
552 updateDate ? sysDate.getHour() : dateTime.getHour(),
553 updateDate ? sysDate.getMinute() : dateTime.getMinute(),
554 updateDate ? sysDate.getSecond() : dateTime.getSecond(), 0,
555 ZoneId.of("UTC" + (utcOffset >= 0 ? "+" : "") + String.format("%02d", utcOffset) + ":00"));
556 updateState("general#" + HeliosEasyControlsBindingConstants.SYS_DATE, new DateTimeType(sysDate));
557 this.sysDate = sysDate;
560 private void setSysDateTime(ZonedDateTime date) throws InterruptedException {
562 this.writeValue(HeliosEasyControlsBindingConstants.DATE,
563 this.formatDate(HeliosEasyControlsBindingConstants.DATE, date));
564 this.writeValue(HeliosEasyControlsBindingConstants.TIME,
565 date.getHour() + ":" + date.getMinute() + ":" + date.getSecond());
566 this.writeValue(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT,
567 Integer.toString(date.getOffset().getTotalSeconds() / 60 / 60));
568 } catch (HeliosException e) {
569 logger.warn("{} encountered Exception when trying to set system date: {}",
570 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
574 protected void setSysDateTime() throws InterruptedException {
575 this.setSysDateTime(ZonedDateTime.now());
578 private void updateBypass(boolean from, boolean month, int val) {
579 BypassDate bypassDate = from ? this.bypassFrom : this.bypassTo;
580 if (bypassDate == null) {
581 bypassDate = new BypassDate();
584 bypassDate.setMonth(val);
587 bypassDate.setDay(val);
589 updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
590 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
592 this.bypassFrom = bypassDate;
595 this.bypassTo = bypassDate;
599 protected void setBypass(boolean from, int day, int month) throws InterruptedException {
600 BypassDate bypassDate = new BypassDate(day, month);
602 this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
603 : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(bypassDate.getDay()));
605 from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH
606 : HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH,
607 Integer.toString(bypassDate.getMonth()));
608 } catch (HeliosException e) {
609 logger.warn("{} encountered Exception when trying to set bypass period: {}",
610 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
615 * Formats the provided date to a string in the device's configured date format
617 * @param variableName the variable name
618 * @param date the date to be formatted
619 * @return a string in the device's configured date format
621 public String formatDate(String variableName, ZonedDateTime date) {
622 String y = Integer.toString(date.getYear());
623 String m = Integer.toString(date.getMonthValue());
624 if (m.length() == 1) {
627 String d = Integer.toString(date.getDayOfMonth());
628 if (d.length() == 1) {
631 if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
632 return d + "." + m + "." + y;
634 switch (this.dateFormat) {
635 case 0: // dd.mm.yyyy
636 return d + "." + m + "." + y;
637 case 1: // mm.dd.yyyy
638 return m + "." + d + "." + y;
639 case 2: // yyyy.mm.dd
640 return y + "." + m + "." + d;
642 return d + "." + m + "." + y;
647 private List<String> getMessages(long bitMask, int bits, String prefix) {
648 ArrayList<String> msg = new ArrayList<String>();
650 for (int i = 0; i < bits; i++) {
651 if ((bitMask & mask) != 0) {
652 msg.add(translationProvider.getText(prefix + i));
660 * Transforms the errors provided by the device into a human readable form (the basis for the
661 * corresponding action)
663 * @return an <code>List</code> of messages indicated by the error flags sent by the device
665 protected List<String> getErrorMessages() {
666 return this.getMessages(this.errors, HeliosEasyControlsBindingConstants.BITS_ERROR_MSG,
667 HeliosEasyControlsBindingConstants.PREFIX_ERROR_MSG);
671 * Transforms the warnings provided by the device into a human readable form (the basis for the
672 * corresponding action)
674 * @return an <code>List</code> of messages indicated by the warning flags sent by the device
676 protected List<String> getWarningMessages() {
677 return this.getMessages(this.warnings, HeliosEasyControlsBindingConstants.BITS_WARNING_MSG,
678 HeliosEasyControlsBindingConstants.PREFIX_WARNING_MSG);
682 * Transforms the infos provided by the device into a human readable form (the basis for the
683 * corresponding action)
685 * @return an <code>List</code> of messages indicated by the info flags sent by the device
687 protected List<String> getInfoMessages() {
688 return this.getMessages(this.infos, HeliosEasyControlsBindingConstants.BITS_INFO_MSG,
689 HeliosEasyControlsBindingConstants.PREFIX_INFO_MSG);
693 * Transforms the status flags provided by the device into a human readable form (the basis for the
694 * corresponding action)
696 * @return an <code>List</code> of messages indicated by the status flags sent by the device
698 protected List<String> getStatusMessages() {
699 ArrayList<String> msg = new ArrayList<String>();
700 if (this.statusFlags.length() == HeliosEasyControlsBindingConstants.BITS_STATUS_MSG) {
701 for (int i = 0; i < HeliosEasyControlsBindingConstants.BITS_STATUS_MSG; i++) {
702 String key = HeliosEasyControlsBindingConstants.PREFIX_STATUS_MSG + i + "."
703 + (this.statusFlags.substring(HeliosEasyControlsBindingConstants.BITS_STATUS_MSG - i - 1,
704 HeliosEasyControlsBindingConstants.BITS_STATUS_MSG - i));
705 String text = translationProvider.getText(key);
706 if (!text.equals(key)) { // there is a text in the properties file (no text => flag is irrelevant)
711 msg.add("Status messages have not yet been read from the device");
717 * Returns a DateTimeType object based on the provided String and the device's configured date format
719 * @param date The date string read from the device
720 * @return A DateTimeType object representing the date or time specified
722 private DateTimeType toDateTime(String date) {
723 String[] dateTimeParts = null;
724 String dateTime = date;
725 dateTimeParts = date.split("\\."); // try to split date components
726 if (dateTimeParts.length == 1) { // time
727 return DateTimeType.valueOf(date);
728 } else if (dateTimeParts.length == 3) { // date - we'll try the device's date format
729 switch (this.dateFormat) {
730 case 0: // dd.mm.yyyy
731 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
733 case 1: // mm.dd.yyyy
734 dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
736 case 2: // yyyy.mm.dd
737 dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
740 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
743 return DateTimeType.valueOf(dateTime);
745 // falling back to default date format (apparently using the configured format has failed)
746 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
747 return DateTimeType.valueOf(dateTime);
750 private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
753 } else if (unit.equals(HeliosVariable.UNIT_DAY)) {
754 return new QuantityType<>(Integer.parseInt(value), Units.DAY);
755 } else if (unit.equals(HeliosVariable.UNIT_HOUR)) {
756 return new QuantityType<>(Integer.parseInt(value), Units.HOUR);
757 } else if (unit.equals(HeliosVariable.UNIT_MIN)) {
758 return new QuantityType<>(Integer.parseInt(value), Units.MINUTE);
759 } else if (unit.equals(HeliosVariable.UNIT_SEC)) {
760 return new QuantityType<>(Integer.parseInt(value), Units.SECOND);
761 } else if (unit.equals(HeliosVariable.UNIT_VOLT)) {
762 return new QuantityType<>(Float.parseFloat(value), Units.VOLT);
763 } else if (unit.equals(HeliosVariable.UNIT_PERCENT)) {
764 return new QuantityType<>(Float.parseFloat(value), Units.PERCENT);
765 } else if (unit.equals(HeliosVariable.UNIT_PPM)) {
766 return new QuantityType<>(Float.parseFloat(value), Units.PARTS_PER_MILLION);
767 } else if (unit.equals(HeliosVariable.UNIT_TEMP)) {
768 return new QuantityType<>(Float.parseFloat(value), SIUnits.CELSIUS);
775 * Prepares the payload for the request
777 * @param payload The String representation of the payload
778 * @return The Register representation of the payload
780 private static ModbusRegisterArray preparePayload(String payload) {
781 // determine number of registers
782 byte[] asciiBytes = payload.getBytes(StandardCharsets.US_ASCII);
783 int bufferLength = asciiBytes.length // ascii characters
785 + ((asciiBytes.length % 2 == 0) ? 1 : 0); // to have even number of bytes
786 assert bufferLength % 2 == 0; // Invariant, ensured above
788 byte[] buffer = new byte[bufferLength];
789 System.arraycopy(asciiBytes, 0, buffer, 0, asciiBytes.length);
790 // Fill in rest of bytes with NUL bytes
791 for (int i = asciiBytes.length; i < buffer.length; i++) {
794 return new ModbusRegisterArray(buffer);
798 * Decodes the Helios device' response and updates the channel with the actual value of the variable
800 * @param response The registers received from the Helios device
801 * @return The value or <tt>null</tt> if an error occurred
803 private void processResponse(HeliosVariable v, ModbusRegisterArray registers) {
804 String r = ModbusBitUtilities.extractStringFromRegisters(registers, 0, registers.size() * 2,
805 StandardCharsets.US_ASCII);
806 String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string
807 // making sure we have a proper response and the response matches the requested variable
808 if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) {
809 if (this.isProperty(v.getName())) {
813 .getText(HeliosEasyControlsBindingConstants.PROPERTIES_PREFIX + v.getName()),
814 v.formatPropertyValue(parts[1], translationProvider));
815 } catch (HeliosException e) {
816 logger.warn("{} encountered Exception when trying to update property: {}",
817 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
820 this.updateState(v, parts[1]);
822 } else { // another variable was read
823 logger.warn("{} tried to read value from variable {} and the result provided by the device was {}",
824 HeliosEasyControlsHandler.class.getSimpleName(), v.getName(), r);
828 private void updateState(HeliosVariable v, String value) {
829 String variableType = v.getType();
830 // System date and time
831 if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE)) {
832 this.updateSysDate(this.toDateTime(value));
833 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME)) {
834 this.updateSysTime(this.toDateTime(value));
835 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT)) {
836 this.updateUtcOffset(Integer.parseInt(value));
838 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY)) {
839 this.updateBypass(true, false, Integer.parseInt(value));
840 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH)) {
841 this.updateBypass(true, true, Integer.parseInt(value));
842 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY)) {
843 this.updateBypass(false, false, Integer.parseInt(value));
844 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH)) {
845 this.updateBypass(false, true, Integer.parseInt(value));
847 Channel channel = getThing().getChannel(v.getGroupAndName());
849 if (channel != null) {
850 itemType = channel.getAcceptedItemType();
851 if (itemType != null) {
852 if (itemType.startsWith("Number:")) {
857 if (((HeliosVariable.TYPE_INTEGER.equals(variableType))
858 || (HeliosVariable.TYPE_FLOAT.equals(variableType))) && (!value.equals("-"))) {
860 if (v.getUnit() == null) {
861 state = DecimalType.valueOf(value);
862 } else { // QuantityType
863 state = this.toQuantityType(value, v.getUnit());
866 updateState(v.getGroupAndName(), state);
867 updateStatus(ThingStatus.ONLINE);
868 // update date format and messages upon read
869 if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE_FORMAT)) {
870 this.dateFormat = Integer.parseInt(value);
871 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.ERRORS)) {
872 this.errors = Long.parseLong(value);
873 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.WARNINGS)) {
874 this.warnings = Integer.parseInt(value);
875 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.INFOS)) {
876 this.infos = Integer.parseInt(value);
882 if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
883 updateState(v.getGroupAndName(), value.equals("1") ? OnOffType.ON : OnOffType.OFF);
887 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
888 updateState(v.getGroupAndName(), StringType.valueOf(value));
889 if (v.getName().equals(HeliosEasyControlsBindingConstants.STATUS_FLAGS)) {
890 this.statusFlags = value;
895 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
896 updateState(v.getGroupAndName(), toDateTime(value));
900 } else { // itemType was null
901 logger.warn("{} couldn't determine item type of variable {}",
902 HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
904 } else { // channel was null
905 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
912 * Logs an error (as a warning entry) and updates the thing status
914 * @param errorMsg The error message to be logged and provided with the Thing's status update
915 * @param status The Thing's new status
917 private void handleError(String errorMsg, ThingStatusDetail status) {
918 updateStatus(ThingStatus.OFFLINE, status, errorMsg);