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.SmartHomeUnits;
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.ModbusRegister;
58 import org.openhab.io.transport.modbus.ModbusRegisterArray;
59 import org.openhab.io.transport.modbus.ModbusWriteRegisterRequestBlueprint;
60 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
64 import com.google.gson.Gson;
65 import com.google.gson.reflect.TypeToken;
68 * The {@link HeliosEasyControlsHandler} is responsible for handling commands, which are
69 * sent to one of the channels.
71 * @author Bernhard Bauer - Initial contribution
74 public class HeliosEasyControlsHandler extends BaseThingHandler {
76 private final Logger logger = LoggerFactory.getLogger(HeliosEasyControlsHandler.class);
78 private @Nullable HeliosEasyControlsConfiguration config;
80 private @Nullable ScheduledFuture<?> pollingJob;
82 private @Nullable Map<String, HeliosVariable> variableMap;
85 * This flag is used to ensure read requests (consisting of a write and subsequent read) are not influenced by
88 private final Map<ModbusSlaveEndpoint, Semaphore> transactionLocks = new ConcurrentHashMap<>();
90 private final Gson gson = new Gson();
92 private @Nullable ModbusCommunicationInterface comms;
94 private int dateFormat = -1;
95 private ZonedDateTime sysDate = ZonedDateTime.now(); // initialize with local system time as a best guess
96 // before reading from device
98 private class BypassDate {
99 // initialization to avoid issues when updating before all variables were read
100 private int month = 1;
103 public void setMonth(int month) {
107 public void setDay(int day) {
111 public DateTimeType toDateTimeType() {
112 return new DateTimeType(ZonedDateTime.of(1900, this.month, this.day, 0, 0, 0, 0, ZoneId.of("UTC+00:00")));
116 private @Nullable BypassDate bypassFrom, bypassTo;
118 public HeliosEasyControlsHandler(Thing thing) {
123 * Reads variable definitions from JSON file and store them in variableMap
125 private void readVariableDefinition() {
126 Type vMapType = new TypeToken<Map<String, HeliosVariable>>() {
128 try (InputStreamReader jsonFile = new InputStreamReader(
129 getClass().getResourceAsStream(HeliosEasyControlsBindingConstants.VARIABLES_DEFINITION_FILE));
130 BufferedReader reader = new BufferedReader(jsonFile)) {
131 this.variableMap = gson.fromJson(reader, vMapType);
132 } catch (IOException e) {
133 this.handleError("Error reading variable definition file", ThingStatusDetail.CONFIGURATION_ERROR);
135 if (variableMap != null) {
136 // add the name to the variable itself
137 for (Map.Entry<String, HeliosVariable> entry : this.variableMap.entrySet()) {
138 entry.getValue().setName(entry.getKey()); // workaround to set the variable name inside the
139 // HeliosVariable object
140 if (!entry.getValue().isOk()) {
141 this.handleError("Variables definition file contains inconsistent data",
142 ThingStatusDetail.CONFIGURATION_ERROR);
146 this.handleError("Variables definition file not found or of illegal format",
147 ThingStatusDetail.CONFIGURATION_ERROR);
152 * Get the endpoint handler from the bridge this handler is connected to
153 * Checks that we're connected to the right type of bridge
155 * @return the endpoint handler or null if the bridge does not exist
157 private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
158 Bridge bridge = getBridge();
159 if (bridge == null) {
160 logger.debug("Bridge is null");
163 if (bridge.getStatus() != ThingStatus.ONLINE) {
164 logger.debug("Bridge is not online");
168 ThingHandler handler = bridge.getHandler();
169 if (handler == null) {
170 logger.debug("Bridge handler is null");
174 if (handler instanceof ModbusEndpointThingHandler) {
175 return (ModbusEndpointThingHandler) handler;
177 logger.debug("Unexpected bridge handler: {}", handler);
183 * Get a reference to the modbus endpoint
185 private void connectEndpoint() {
186 if (this.comms != null) {
190 ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
191 if (slaveEndpointThingHandler == null) {
192 @SuppressWarnings("null")
193 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
194 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
195 String.format("Bridge '%s' is offline", label));
196 logger.debug("No bridge handler available -- aborting init for {}", label);
200 comms = slaveEndpointThingHandler.getCommunicationInterface();
203 @SuppressWarnings("null")
204 String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
206 String.format("Bridge '%s' not completely initialized", label));
207 logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
213 public void initialize() {
214 this.config = getConfigAs(HeliosEasyControlsConfiguration.class);
215 this.readVariableDefinition();
216 this.connectEndpoint();
217 if ((this.comms != null) && (this.variableMap != null) && (this.config != null)) {
218 this.transactionLocks.putIfAbsent(this.comms.getEndpoint(), new Semaphore(1, true));
219 updateStatus(ThingStatus.UNKNOWN);
221 // background initialization
222 scheduler.execute(() -> {
223 readValue(HeliosEasyControlsBindingConstants.DATE_FORMAT);
224 // status will be updated to ONLINE by the read callback function (via processResponse)
227 // poll for status updates regularly
228 HeliosEasyControlsConfiguration config = this.config;
229 if (config != null) {
230 this.pollingJob = scheduler.scheduleWithFixedDelay(() -> {
231 if (variableMap != null) {
232 for (Map.Entry<String, HeliosVariable> entry : variableMap.entrySet()) {
233 if (this.isProperty(entry.getKey()) || isLinked(entry.getValue().getGroupAndName())
234 || HeliosEasyControlsBindingConstants.ALWAYS_UPDATE_VARIABLES
235 .contains(entry.getKey())) {
236 readValue(entry.getKey());
240 handleError("Variable definition is null", ThingStatusDetail.CONFIGURATION_ERROR);
242 }, config.getRefreshInterval(), config.getRefreshInterval(), TimeUnit.MILLISECONDS);
244 } else { // at least one null assertion has failed, let's log the problem and update the thing status
245 if (this.comms == null) {
246 this.handleError("Modbus communication interface is unavailable",
247 ThingStatusDetail.COMMUNICATION_ERROR);
249 if (this.variableMap == null) {
250 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
252 if (this.config == null) {
253 this.handleError("Binding configuration is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
259 public void dispose() {
260 if (this.pollingJob != null) {
261 this.pollingJob.cancel(true);
267 public void handleCommand(ChannelUID channelUID, Command command) {
268 String channelId = channelUID.getIdWithoutGroup();
269 if (command instanceof RefreshType) {
270 if (channelId.equals(HeliosEasyControlsBindingConstants.SYS_DATE)) {
271 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.DATE));
272 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.TIME));
273 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_FROM)) {
274 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY));
275 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH));
276 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_TO)) {
277 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY));
278 scheduler.submit(() -> readValue(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH));
280 scheduler.submit(() -> readValue(channelId));
282 } else { // write command
284 if (command instanceof OnOffType) {
285 value = command == OnOffType.ON ? "1" : "0";
286 } else if (command instanceof DateTimeType) {
287 ZonedDateTime d = ((DateTimeType) command).getZonedDateTime();
288 if (channelId.equals(HeliosEasyControlsBindingConstants.SYS_DATE)) {
290 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_FROM)) {
291 this.setBypass(true, d.getDayOfMonth(), d.getMonthValue());
292 } else if (channelId.equals(HeliosEasyControlsBindingConstants.BYPASS_TO)) {
293 this.setBypass(false, d.getDayOfMonth(), d.getMonthValue());
295 value = formatDate(channelId, ((DateTimeType) command).getZonedDateTime());
297 } else if ((command instanceof DecimalType) || (command instanceof StringType)) {
298 value = command.toString();
299 } else if (command instanceof QuantityType<?>) {
300 // convert item's unit to the Helios device's unit
301 Map<String, HeliosVariable> variableMap = this.variableMap;
302 if (variableMap != null) {
303 String unit = variableMap.get(channelId).getUnit();
304 QuantityType<?> val = (QuantityType<?>) command;
307 case HeliosVariable.UNIT_DAY:
308 val = val.toUnit(SmartHomeUnits.DAY);
310 case HeliosVariable.UNIT_HOUR:
311 val = val.toUnit(SmartHomeUnits.HOUR);
313 case HeliosVariable.UNIT_MIN:
314 val = val.toUnit(SmartHomeUnits.MINUTE);
316 case HeliosVariable.UNIT_SEC:
317 val = val.toUnit(SmartHomeUnits.SECOND);
319 case HeliosVariable.UNIT_VOLT:
320 val = val.toUnit(SmartHomeUnits.VOLT);
322 case HeliosVariable.UNIT_PERCENT:
323 val = val.toUnit(SmartHomeUnits.PERCENT);
325 case HeliosVariable.UNIT_PPM:
326 val = val.toUnit(SmartHomeUnits.PARTS_PER_MILLION);
328 case HeliosVariable.UNIT_TEMP:
329 val = val.toUnit(SIUnits.CELSIUS);
332 value = val != null ? String.valueOf(val.doubleValue()) : null; // ignore the UoM
337 final String v = value;
338 scheduler.submit(() -> {
340 writeValue(channelId, v);
341 if (variableMap != null) {
342 HeliosVariable variable = variableMap.get(channelId);
343 if (variable != null) {
344 updateState(variable, v);
345 updateStatus(ThingStatus.ONLINE);
348 } catch (HeliosException e) {
349 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
350 "Writing value " + v + "to channel " + channelId + " failed: " + e.getMessage());
358 public Collection<Class<? extends ThingHandlerService>> getServices() {
359 return Collections.singleton(HeliosEasyControlsActions.class);
363 * Checks if the provided variable name is a property
365 * @param variableName The variable's name
366 * @return true if the variable is a property
368 private boolean isProperty(String variableName) {
369 return HeliosEasyControlsBindingConstants.PROPERTY_NAMES.contains(variableName);
373 * Writes a variable value to the Helios device
375 * @param variableName The variable name
376 * @param value The new value
377 * @return The value if the transaction succeeded, <tt>null</tt> otherwise
378 * @throws HeliosException Thrown if the variable is read-only or the provided value is out of range
380 public void writeValue(String variableName, String value) throws HeliosException {
381 if (this.variableMap == null) {
382 this.handleError("Variable definition is unavailable.", ThingStatusDetail.CONFIGURATION_ERROR);
385 Map<String, HeliosVariable> variableMap = this.variableMap;
386 if (variableMap != null) {
387 HeliosVariable v = variableMap.get(variableName);
389 if (!v.hasWriteAccess()) {
390 throw new HeliosException("Variable " + variableName + " is read-only");
391 } else if (!v.isInAllowedRange(value)) {
392 throw new HeliosException(
393 "Value " + value + " is outside of allowed range of variable " + variableName);
394 } else if (this.comms != null) {
396 String payload = v.getVariableString() + "=" + value;
397 ModbusCommunicationInterface comms = this.comms;
399 final Semaphore lock = transactionLocks.get(comms.getEndpoint());
402 comms.submitOneTimeWrite(
403 new ModbusWriteRegisterRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
404 HeliosEasyControlsBindingConstants.START_ADDRESS,
405 new ModbusRegisterArray(preparePayload(payload)), true,
406 HeliosEasyControlsBindingConstants.MAX_TRIES),
409 updateStatus(ThingStatus.ONLINE);
412 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
413 "Error writing to device: " + failureInfo.getCause().getMessage());
415 } catch (InterruptedException e) {
417 "{} encountered Exception when trying to lock Semaphore for writing variable {} to the device: {}",
418 HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
421 } else { // comms is null
422 this.handleError("Modbus communication interface is null", ThingStatusDetail.COMMUNICATION_ERROR);
429 * Read a variable from the Helios device
431 * @param variableName The variable name
434 public void readValue(String variableName) {
435 Map<String, HeliosVariable> variableMap = this.variableMap;
436 ModbusCommunicationInterface comms = this.comms;
437 if ((comms != null) && (variableMap != null)) {
438 final Semaphore lock = transactionLocks.get(comms.getEndpoint());
439 HeliosVariable v = variableMap.get(variableName);
440 if (v.hasReadAccess()) {
442 lock.acquire(); // will block until lock is available
443 } catch (InterruptedException e) {
444 logger.warn("{} encountered Exception when trying to read variable {} from the device: {}",
445 HeliosEasyControlsHandler.class.getSimpleName(), variableName, e.getMessage());
448 // write variable name to register
449 String payload = v.getVariableString();
450 comms.submitOneTimeWrite(new ModbusWriteRegisterRequestBlueprint(
451 HeliosEasyControlsBindingConstants.UNIT_ID, HeliosEasyControlsBindingConstants.START_ADDRESS,
452 new ModbusRegisterArray(preparePayload(payload)), true,
453 HeliosEasyControlsBindingConstants.MAX_TRIES), result -> {
454 comms.submitOneTimePoll(
455 new ModbusReadRequestBlueprint(HeliosEasyControlsBindingConstants.UNIT_ID,
456 ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
457 HeliosEasyControlsBindingConstants.START_ADDRESS, v.getCount(),
458 HeliosEasyControlsBindingConstants.MAX_TRIES),
461 Optional<ModbusRegisterArray> registers = pollResult.getRegisters();
462 if (registers.isPresent()) {
463 processResponse(v, registers.get());
467 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
468 "Error reading from device: " + failureInfo.getCause().getMessage());
472 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
473 "Error writing to device: " + failureInfo.getCause().getMessage());
479 if (this.comms == null) {
480 this.handleError("Modbus communication interface is unavailable",
481 ThingStatusDetail.COMMUNICATION_ERROR);
483 if (variableMap == null) {
484 this.handleError("Variable definition is unavailable", ThingStatusDetail.CONFIGURATION_ERROR);
489 private void updateSysDate(DateTimeType dateTime) {
490 this.updateSysDateTime(dateTime.getZonedDateTime(), true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
493 private void updateSysTime(DateTimeType dateTime) {
494 this.updateSysDateTime(dateTime.getZonedDateTime(), false, sysDate.getOffset().getTotalSeconds() / 60 / 60);
497 private void updateUtcOffset(int utcOffset) {
498 this.updateSysDateTime(this.sysDate, true, sysDate.getOffset().getTotalSeconds() / 60 / 60);
501 private void updateSysDateTime(ZonedDateTime dateTime, boolean updateDate, int utcOffset) {
502 ZonedDateTime sysDate = this.sysDate;
503 sysDate = ZonedDateTime.of(updateDate ? dateTime.getYear() : sysDate.getYear(),
504 updateDate ? dateTime.getMonthValue() : sysDate.getMonthValue(),
505 updateDate ? dateTime.getDayOfMonth() : sysDate.getDayOfMonth(),
506 updateDate ? sysDate.getHour() : dateTime.getHour(),
507 updateDate ? sysDate.getMinute() : dateTime.getMinute(),
508 updateDate ? sysDate.getSecond() : dateTime.getSecond(), 0,
509 ZoneId.of("UTC" + (utcOffset >= 0 ? "+" : "") + String.format("%02d", utcOffset) + ":00"));
510 updateState("general#" + HeliosEasyControlsBindingConstants.SYS_DATE, new DateTimeType(sysDate));
511 this.sysDate = sysDate;
514 private void setSysDateTime(ZonedDateTime date) {
516 this.writeValue(HeliosEasyControlsBindingConstants.DATE,
517 this.formatDate(HeliosEasyControlsBindingConstants.DATE, date));
518 this.writeValue(HeliosEasyControlsBindingConstants.TIME,
519 date.getHour() + ":" + date.getMinute() + ":" + date.getSecond());
520 this.writeValue(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT,
521 Integer.toString(date.getOffset().getTotalSeconds() / 60 / 60));
522 } catch (HeliosException e) {
523 logger.warn("{} encountered Exception when trying to set system date: {}",
524 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
528 protected void setSysDateTime() {
529 this.setSysDateTime(ZonedDateTime.now());
532 private void updateBypass(boolean from, boolean month, int val) {
533 BypassDate bypassDate = from ? this.bypassFrom : this.bypassTo;
534 if (bypassDate == null) {
535 bypassDate = new BypassDate();
538 bypassDate.setMonth(val);
541 bypassDate.setDay(val);
543 updateState("unitConfig#" + (from ? HeliosEasyControlsBindingConstants.BYPASS_FROM
544 : HeliosEasyControlsBindingConstants.BYPASS_TO), bypassDate.toDateTimeType());
546 this.bypassFrom = bypassDate;
549 this.bypassTo = bypassDate;
553 protected void setBypass(boolean from, int day, int month) {
555 this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY
556 : HeliosEasyControlsBindingConstants.BYPASS_TO_DAY, Integer.toString(day));
557 this.writeValue(from ? HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH
558 : HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH, Integer.toString(month));
559 } catch (HeliosException e) {
560 logger.warn("{} encountered Exception when trying to set bypass period: {}",
561 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
566 * Formats the provided date to a string in the device's configured date format
568 * @param variableName the variable name
569 * @param date the date to be formatted
570 * @return a string in the device's configured date format
572 public String formatDate(String variableName, ZonedDateTime date) {
573 String y = Integer.toString(date.getYear());
574 String m = Integer.toString(date.getMonthValue());
575 if (m.length() == 1) {
578 String d = Integer.toString(date.getDayOfMonth());
579 if (d.length() == 1) {
582 if (variableName.equals(HeliosEasyControlsBindingConstants.DATE)) { // fixed format for writing the system date
583 return d + "." + m + "." + y;
585 switch (this.dateFormat) {
586 case 0: // dd.mm.yyyy
587 return d + "." + m + "." + y;
588 case 1: // mm.dd.yyyy
589 return m + "." + d + "." + y;
590 case 2: // yyyy.mm.dd
591 return y + "." + m + "." + d;
593 return d + "." + m + "." + y;
599 * Returns a DateTimeType object based on the provided String and the device's configured date format
601 * @param date The date string read from the device
602 * @return A DateTimeType object representing the date or time specified
604 private DateTimeType toDateTime(String date) {
605 String[] dateTimeParts = null;
606 String dateTime = date;
607 dateTimeParts = date.split("\\."); // try to split date components
608 if (dateTimeParts.length == 1) { // time
609 return DateTimeType.valueOf(date);
610 } else if (dateTimeParts.length == 3) { // date - we'll try the device's date format
611 switch (this.dateFormat) {
612 case 0: // dd.mm.yyyy
613 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
615 case 1: // mm.dd.yyyy
616 dateTime = dateTimeParts[2] + "-" + dateTimeParts[0] + "-" + dateTimeParts[1];
618 case 2: // yyyy.mm.dd
619 dateTime = dateTimeParts[0] + "-" + dateTimeParts[1] + "-" + dateTimeParts[2];
622 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
625 return DateTimeType.valueOf(dateTime);
627 // falling back to default date format (apparently using the configured format has failed)
628 dateTime = dateTimeParts[2] + "-" + dateTimeParts[1] + "-" + dateTimeParts[0];
629 return DateTimeType.valueOf(dateTime);
632 private @Nullable QuantityType<?> toQuantityType(String value, @Nullable String unit) {
635 } else if (unit.equals(HeliosVariable.UNIT_DAY)) {
636 return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.DAY);
637 } else if (unit.equals(HeliosVariable.UNIT_HOUR)) {
638 return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.HOUR);
639 } else if (unit.equals(HeliosVariable.UNIT_MIN)) {
640 return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.MINUTE);
641 } else if (unit.equals(HeliosVariable.UNIT_SEC)) {
642 return new QuantityType<>(Integer.parseInt(value), SmartHomeUnits.SECOND);
643 } else if (unit.equals(HeliosVariable.UNIT_VOLT)) {
644 return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.VOLT);
645 } else if (unit.equals(HeliosVariable.UNIT_PERCENT)) {
646 return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PERCENT);
647 } else if (unit.equals(HeliosVariable.UNIT_PPM)) {
648 return new QuantityType<>(Float.parseFloat(value), SmartHomeUnits.PARTS_PER_MILLION);
649 } else if (unit.equals(HeliosVariable.UNIT_TEMP)) {
650 return new QuantityType<>(Float.parseFloat(value), SIUnits.CELSIUS);
657 * Prepares the payload for the request
659 * @param payload The String representation of the payload
660 * @return The Register representation of the payload
662 private ModbusRegister[] preparePayload(String payload) {
664 // determine number of registers
665 int l = (payload.length() + 1) / 2; // +1 because we need to include at least one termination symbol 0x00
666 if ((payload.length() + 1) % 2 != 0) {
670 ModbusRegister reg[] = new ModbusRegister[l];
671 byte[] b = payload.getBytes();
673 for (int i = 0; i < reg.length; i++) {
674 byte b1 = ch < b.length ? b[ch] : (byte) 0x00; // terminate with 0x00 if at the end of the payload
676 byte b2 = ch < b.length ? b[ch] : (byte) 0x00;
678 reg[i] = new ModbusRegister(b1, b2);
684 * Decodes the Helios device' response and updates the channel with the actual value of the variable
686 * @param response The registers received from the Helios device
687 * @return The value or <tt>null</tt> if an error occurred
689 private void processResponse(HeliosVariable v, ModbusRegisterArray registers) {
690 String r = ModbusBitUtilities
691 .extractStringFromRegisters(registers, 0, registers.size() * 2, StandardCharsets.US_ASCII).toString();
692 String[] parts = r.split("=", 2); // remove the part "vXXXX=" from the string
693 // making sure we have a proper response and the response matches the requested variable
694 if ((parts.length == 2) && (v.getVariableString().equals(parts[0]))) {
695 if (this.isProperty(v.getName())) {
697 updateProperty(v.getName(), v.formatPropertyValue(parts[1]));
698 } catch (HeliosException e) {
699 logger.warn("{} encountered Exception when trying to update property: {}",
700 HeliosEasyControlsHandler.class.getSimpleName(), e.getMessage());
703 this.updateState(v, parts[1]);
705 } else { // another variable was read
706 logger.warn("{} tried to read value from variable {} and the result provided by the device was {}",
707 HeliosEasyControlsHandler.class.getSimpleName(), v.getName(), r);
711 private void updateState(HeliosVariable v, String value) {
712 String variableType = v.getType();
713 // System date and time
714 if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE)) {
715 this.updateSysDate(this.toDateTime(value));
716 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME)) {
717 this.updateSysTime(this.toDateTime(value));
718 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.TIME_ZONE_DIFFERENCE_TO_GMT)) {
719 this.updateUtcOffset(Integer.parseInt(value));
721 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_DAY)) {
722 this.updateBypass(true, false, Integer.parseInt(value));
723 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_FROM_MONTH)) {
724 this.updateBypass(true, true, Integer.parseInt(value));
725 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_DAY)) {
726 this.updateBypass(false, false, Integer.parseInt(value));
727 } else if (v.getName().equals(HeliosEasyControlsBindingConstants.BYPASS_TO_MONTH)) {
728 this.updateBypass(false, true, Integer.parseInt(value));
730 Channel channel = getThing().getChannel(v.getGroupAndName());
732 if (channel != null) {
733 itemType = channel.getAcceptedItemType();
734 if (itemType != null) {
735 if (itemType.startsWith("Number:")) {
740 if (((variableType.equals(HeliosVariable.TYPE_INTEGER))
741 || (variableType == HeliosVariable.TYPE_FLOAT)) && (!value.equals("-"))) {
743 if (v.getUnit() == null) {
744 state = DecimalType.valueOf(value);
745 } else { // QuantityType
746 state = this.toQuantityType(value, v.getUnit());
749 updateState(v.getGroupAndName(), state);
750 updateStatus(ThingStatus.ONLINE);
751 // update date format and UTC offset upon read
752 if (v.getName().equals(HeliosEasyControlsBindingConstants.DATE_FORMAT)) {
753 this.dateFormat = Integer.parseInt(value);
759 if (variableType.equals(HeliosVariable.TYPE_INTEGER)) {
760 updateState(v.getGroupAndName(), value.equals("1") ? OnOffType.ON : OnOffType.OFF);
764 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
765 updateState(v.getGroupAndName(), StringType.valueOf(value));
769 if (variableType.equals(HeliosVariable.TYPE_STRING)) {
770 updateState(v.getGroupAndName(), toDateTime(value));
774 } else { // itemType was null
775 logger.warn("{} couldn't determine item type of variable {}",
776 HeliosEasyControlsHandler.class.getSimpleName(), v.getName());
778 } else { // channel was null
779 logger.warn("{} couldn't find channel for variable {}", HeliosEasyControlsHandler.class.getSimpleName(),
786 * Logs an error (as a warning entry) and updates the thing status
788 * @param errorMsg The error message to be logged and provided with the Thing's status update
789 * @param status The Thing's new status
791 private void handleError(String errorMsg, ThingStatusDetail status) {
792 updateStatus(ThingStatus.OFFLINE, status, errorMsg);