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.nibeheatpump.internal.handler;
15 import static org.openhab.binding.nibeheatpump.internal.NibeHeatPumpBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27 import java.util.stream.Collectors;
29 import org.openhab.binding.nibeheatpump.internal.NibeHeatPumpCommandResult;
30 import org.openhab.binding.nibeheatpump.internal.NibeHeatPumpException;
31 import org.openhab.binding.nibeheatpump.internal.config.NibeHeatPumpConfiguration;
32 import org.openhab.binding.nibeheatpump.internal.connection.NibeHeatPumpConnector;
33 import org.openhab.binding.nibeheatpump.internal.connection.NibeHeatPumpEventListener;
34 import org.openhab.binding.nibeheatpump.internal.connection.SerialConnector;
35 import org.openhab.binding.nibeheatpump.internal.connection.SimulatorConnector;
36 import org.openhab.binding.nibeheatpump.internal.connection.UDPConnector;
37 import org.openhab.binding.nibeheatpump.internal.message.ModbusDataReadOutMessage;
38 import org.openhab.binding.nibeheatpump.internal.message.ModbusReadRequestMessage;
39 import org.openhab.binding.nibeheatpump.internal.message.ModbusReadResponseMessage;
40 import org.openhab.binding.nibeheatpump.internal.message.ModbusValue;
41 import org.openhab.binding.nibeheatpump.internal.message.ModbusWriteRequestMessage;
42 import org.openhab.binding.nibeheatpump.internal.message.ModbusWriteResponseMessage;
43 import org.openhab.binding.nibeheatpump.internal.message.NibeHeatPumpMessage;
44 import org.openhab.binding.nibeheatpump.internal.models.PumpModel;
45 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation;
46 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation.NibeDataType;
47 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation.Type;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.OpenClosedType;
52 import org.openhab.core.library.types.QuantityType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.library.types.UpDownType;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.ThingTypeUID;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.UnDefType;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
69 * The {@link NibeHeatPumpHandler} is responsible for handling commands, which
70 * are sent to one of the channels.
72 * @author Pauli Anttila - Initial contribution
74 public class NibeHeatPumpHandler extends BaseThingHandler implements NibeHeatPumpEventListener {
76 private static final int TIMEOUT = 4500;
77 private final Logger logger = LoggerFactory.getLogger(NibeHeatPumpHandler.class);
78 private final PumpModel pumpModel;
79 private final SerialPortManager serialPortManager;
80 private final List<Integer> itemsToPoll = Collections.synchronizedList(new ArrayList<>());
81 private final List<Integer> itemsToEnableWrite = new ArrayList<>();
82 private final Map<Integer, CacheObject> stateMap = Collections.synchronizedMap(new HashMap<>());
83 private NibeHeatPumpConfiguration configuration;
84 private NibeHeatPumpConnector connector;
85 private boolean reconnectionRequest;
86 private NibeHeatPumpCommandResult writeResult;
87 private NibeHeatPumpCommandResult readResult;
88 private final Runnable pollingRunnable = new Runnable() {
91 if (!configuration.enableReadCommands) {
92 logger.trace("All read commands denied, skip polling!");
97 synchronized (itemsToPoll) {
98 items = new ArrayList<>(itemsToPoll);
101 for (int item : items) {
102 if (connector != null && connector.isConnected()
103 && getThing().getStatusInfo().getStatus() == ThingStatus.ONLINE) {
104 CacheObject oldValue = stateMap.get(item);
106 || (oldValue.lastUpdateTime + refreshIntervalMillis()) < System.currentTimeMillis()) {
107 // it's time to refresh data
108 logger.debug("Time to refresh variable '{}' data", item);
110 ModbusReadRequestMessage request = new ModbusReadRequestMessage.MessageBuilder()
111 .coilAddress(item).build();
114 readResult = sendMessageToNibe(request);
115 ModbusReadResponseMessage result = (ModbusReadResponseMessage) readResult.get(TIMEOUT,
116 TimeUnit.MILLISECONDS);
117 if (result != null) {
118 if (request.getCoilAddress() != result.getCoilAddress()) {
119 logger.debug("Data from wrong register '{}' received, expected '{}'",
120 result.getCoilAddress(), request.getCoilAddress());
122 // update variable anyway
123 handleVariableUpdate(pumpModel, result.getValueAsModbusValue());
125 } catch (TimeoutException e) {
126 logger.debug("Message sending to heat pump failed, no response");
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
128 } catch (InterruptedException e) {
129 logger.debug("Message sending to heat pump failed, sending interrupted");
130 } catch (NibeHeatPumpException e) {
131 logger.debug("Message sending to heat pump failed, exception {}", e.getMessage());
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
141 private ScheduledFuture<?> connectorTask;
142 private ScheduledFuture<?> pollingJob;
143 private long lastUpdateTime = 0;
145 public NibeHeatPumpHandler(Thing thing, PumpModel pumpModel, SerialPortManager serialPortManager) {
147 this.pumpModel = pumpModel;
148 this.serialPortManager = serialPortManager;
151 private NibeHeatPumpConnector getConnector() throws NibeHeatPumpException {
152 ThingTypeUID type = thing.getThingTypeUID();
154 if (THING_TYPE_F1X45_UDP.equals(type) || THING_TYPE_F1X55_UDP.equals(type) || THING_TYPE_F750_UDP.equals(type)
155 || THING_TYPE_F470_UDP.equals(type)) {
156 return new UDPConnector();
157 } else if (THING_TYPE_F1X45_SERIAL.equals(type) || THING_TYPE_F1X55_SERIAL.equals(type)
158 || THING_TYPE_F750_SERIAL.equals(type) || THING_TYPE_F470_SERIAL.equals(type)) {
159 return new SerialConnector(serialPortManager);
160 } else if (THING_TYPE_F1X45_SIMULATOR.equals(type) || THING_TYPE_F1X55_SIMULATOR.equals(type)
161 || THING_TYPE_F750_SIMULATOR.equals(type) || THING_TYPE_F470_SIMULATOR.equals(type)) {
162 return new SimulatorConnector();
165 String description = String.format("Unknown connector type %s", type);
166 throw new NibeHeatPumpException(description);
170 public void handleCommand(ChannelUID channelUID, Command command) {
171 logger.debug("Received channel: {}, command: {}", channelUID, command);
173 int coilAddress = parseCoilAddressFromChannelUID(channelUID);
175 if (command.equals(RefreshType.REFRESH)) {
176 logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
177 clearCache(coilAddress);
181 if (!configuration.enableWriteCommands) {
183 "All write commands denied, ignoring command! Change Nibe heat pump binding configuration if you want to enable write commands.");
187 if (!itemsToEnableWrite.contains(coilAddress)) {
189 "Write commands to register '{}' not allowed, ignoring command! Add this register to Nibe heat pump binding configuration if you want to enable write commands.",
194 if (connector != null) {
195 VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
196 logger.debug("Using variable information for register {}: {}", coilAddress, variableInfo);
198 if (variableInfo != null && variableInfo.type == VariableInformation.Type.SETTING) {
200 int value = convertCommandToNibeValue(variableInfo, command);
202 ModbusWriteRequestMessage msg = new ModbusWriteRequestMessage.MessageBuilder()
203 .coilAddress(coilAddress).value(value).build();
205 writeResult = sendMessageToNibe(msg);
206 ModbusWriteResponseMessage result = (ModbusWriteResponseMessage) writeResult.get(TIMEOUT,
207 TimeUnit.MILLISECONDS);
208 if (result != null) {
209 if (result.isSuccessfull()) {
210 logger.debug("Write message sending to heat pump succeeded");
212 logger.error("Message sending to heat pump failed, value not accepted by the heat pump");
215 logger.debug("Something weird happen, result for write command is null");
217 } catch (TimeoutException e) {
218 logger.warn("Message sending to heat pump failed, no response");
219 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
220 "No response received from the heat pump");
221 } catch (InterruptedException e) {
222 logger.debug("Message sending to heat pump failed, sending interrupted");
223 } catch (NibeHeatPumpException e) {
224 logger.debug("Message sending to heat pump failed, exception {}", e.getMessage());
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
226 } catch (CommandTypeNotSupportedException e) {
227 logger.warn("Unsupported command type {} received for channel {}, coil address {}.",
228 command.getClass().getName(), channelUID.getId(), coilAddress);
233 // Clear cache value to refresh coil data from the pump.
234 // We might not know if write message have succeed or not, so let's always refresh it.
235 logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
236 clearCache(coilAddress);
238 logger.debug("Command to channel '{}' rejected, because item is read only parameter", channelUID);
241 logger.debug("No connection to heat pump");
242 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
247 public void channelLinked(ChannelUID channelUID) {
248 logger.debug("channelLinked: {}", channelUID);
250 // Add channel to polling loop
251 int coilAddress = parseCoilAddressFromChannelUID(channelUID);
252 synchronized (itemsToPoll) {
253 if (!itemsToPoll.contains(coilAddress)) {
254 logger.debug("New channel '{}' found, register '{}'", channelUID.getAsString(), coilAddress);
255 itemsToPoll.add(coilAddress);
258 clearCache(coilAddress);
262 public void channelUnlinked(ChannelUID channelUID) {
263 logger.debug("channelUnlinked: {}", channelUID);
265 // remove channel from polling loop
266 int coilAddress = parseCoilAddressFromChannelUID(channelUID);
267 synchronized (itemsToPoll) {
268 itemsToPoll.removeIf(c -> c.equals(coilAddress));
272 private int parseCoilAddressFromChannelUID(ChannelUID channelUID) {
273 if (channelUID.getId().contains("#")) {
274 String[] parts = channelUID.getId().split("#");
275 return Integer.parseInt(parts[parts.length - 1]);
277 return Integer.parseInt(channelUID.getId());
282 public void initialize() {
283 logger.debug("Initialized Nibe Heat Pump device handler for {}", getThing().getUID());
284 configuration = getConfigAs(NibeHeatPumpConfiguration.class);
285 logger.debug("Using configuration: {}", configuration.toString());
288 parseWriteEnabledItems();
289 connector = getConnector();
290 } catch (IllegalArgumentException | NibeHeatPumpException e) {
291 String description = String.format("Illegal configuration, %s", e.getMessage());
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
297 itemsToPoll.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
298 int coilAddress = parseCoilAddressFromChannelUID(c.getUID());
299 logger.debug("Linked channel '{}' found, register '{}'", c.getUID().getAsString(), coilAddress);
301 }).filter(c -> c != 0).collect(Collectors.toSet()));
303 logger.debug("Linked registers {}: {}", itemsToPoll.size(), itemsToPoll);
307 if (connectorTask == null || connectorTask.isCancelled()) {
308 connectorTask = scheduler.scheduleWithFixedDelay(() -> {
309 if (reconnectionRequest) {
310 logger.debug("Restarting requested, restarting...");
311 reconnectionRequest = false;
315 logger.debug("Checking Nibe Heat pump connection, thing status = {}", thing.getStatus());
317 }, 0, 10, TimeUnit.SECONDS);
321 private void connect() {
322 if (!connector.isConnected()) {
323 logger.debug("Connecting to heat pump");
325 connector.addEventListener(this);
326 connector.connect(configuration);
327 updateStatus(ThingStatus.ONLINE);
329 if (pollingJob == null || pollingJob.isCancelled()) {
330 logger.debug("Start refresh task, interval={}sec", 1);
331 pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 1, TimeUnit.SECONDS);
333 } catch (NibeHeatPumpException e) {
334 logger.debug("Error occurred when connecting to heat pump, exception {}", e.getMessage());
335 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
338 logger.debug("Connection to heat pump already open");
343 public void dispose() {
344 logger.debug("Thing {} disposed.", getThing().getUID());
346 if (connectorTask != null && !connectorTask.isCancelled()) {
347 connectorTask.cancel(true);
348 connectorTask = null;
354 private void closeConnection() {
355 logger.debug("Closing connection to the heat pump");
357 if (pollingJob != null && !pollingJob.isCancelled()) {
358 pollingJob.cancel(true);
362 if (connector != null) {
363 connector.removeEventListener(this);
364 connector.disconnect();
368 private long refreshIntervalMillis() {
369 return configuration.refreshInterval * 1000;
372 private int convertCommandToNibeValue(VariableInformation variableInfo, Command command)
373 throws CommandTypeNotSupportedException {
376 if (command instanceof DecimalType || command instanceof QuantityType || command instanceof StringType) {
378 if (command instanceof DecimalType) {
379 v = ((DecimalType) command).toBigDecimal();
380 } else if (command instanceof QuantityType) {
381 v = ((QuantityType) command).toBigDecimal();
383 v = new BigDecimal(command.toString());
385 int decimals = (int) Math.log10(variableInfo.factor);
386 value = v.movePointRight(decimals).intValue();
387 } else if ((command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType)
388 && variableInfo.factor == 1) {
389 value = (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
390 || command.equals(OpenClosedType.OPEN)) ? 1 : 0;
392 throw new CommandTypeNotSupportedException();
398 private void parseWriteEnabledItems() throws IllegalArgumentException {
399 itemsToEnableWrite.clear();
400 if (configuration.enableWriteCommands && configuration.enableWriteCommandsToRegisters != null
401 && configuration.enableWriteCommandsToRegisters.length() > 0) {
402 String[] items = configuration.enableWriteCommandsToRegisters.replace(" ", "").split(",");
403 for (String item : items) {
405 int coilAddress = Integer.parseInt(item);
406 VariableInformation variableInformation = VariableInformation.getVariableInfo(pumpModel,
408 if (variableInformation == null) {
409 String description = String.format("Unknown register %s", coilAddress);
410 throw new IllegalArgumentException(description);
412 itemsToEnableWrite.add(coilAddress);
413 } catch (NumberFormatException e) {
414 String description = String.format("Illegal register %s", item);
415 throw new IllegalArgumentException(description);
419 logger.debug("Enabled registers for write commands: {}", itemsToEnableWrite);
422 private State convertNibeValueToState(VariableInformation variableInfo, int value, String acceptedItemType) {
423 State state = UnDefType.UNDEF;
426 NibeDataType dataType = variableInfo.dataType;
427 int decimals = (int) Math.log10(variableInfo.factor);
430 x = Byte.toUnsignedLong((byte) (value & 0xFF));
433 x = Short.toUnsignedLong((short) (value & 0xFFFF));
436 x = Integer.toUnsignedLong(value);
439 x = (byte) (value & 0xFF);
442 x = (short) (value & 0xFFFF);
450 BigDecimal converted = new BigDecimal(x).movePointLeft(decimals).setScale(decimals, RoundingMode.HALF_EVEN);
452 if ("String".equalsIgnoreCase(acceptedItemType)) {
453 state = new StringType(converted.toString());
455 } else if ("Switch".equalsIgnoreCase(acceptedItemType)) {
456 state = converted.intValue() == 0 ? OnOffType.OFF : OnOffType.ON;
458 } else if ("Number".equalsIgnoreCase(acceptedItemType)) {
459 state = new DecimalType(converted);
465 private void clearCache() {
470 private void clearCache(int coilAddress) {
471 stateMap.put(coilAddress, null);
474 private synchronized NibeHeatPumpCommandResult sendMessageToNibe(NibeHeatPumpMessage msg)
475 throws NibeHeatPumpException {
476 logger.debug("Sending message: {}", msg);
477 connector.sendDatagram(msg);
478 return new NibeHeatPumpCommandResult();
482 public void msgReceived(NibeHeatPumpMessage msg) {
484 if (logger.isTraceEnabled()) {
485 logger.trace("Received raw data: {}", msg.toHexString());
488 logger.debug("Received message: {}", msg);
490 updateStatus(ThingStatus.ONLINE);
492 if (msg instanceof ModbusReadResponseMessage) {
493 handleReadResponseMessage((ModbusReadResponseMessage) msg);
494 } else if (msg instanceof ModbusWriteResponseMessage) {
495 handleWriteResponseMessage((ModbusWriteResponseMessage) msg);
496 } else if (msg instanceof ModbusDataReadOutMessage) {
497 handleDataReadOutMessage((ModbusDataReadOutMessage) msg);
499 logger.debug("Received unknown message: {}", msg.toString());
501 } catch (Exception e) {
502 logger.debug("Error occurred when parsing received message, reason: {}", e.getMessage());
507 public void errorOccurred(String error) {
508 logger.debug("Error '{}' occurred, re-establish the connection", error);
509 reconnectionRequest = true;
510 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
513 private void handleReadResponseMessage(ModbusReadResponseMessage msg) {
514 if (readResult != null) {
519 private void handleWriteResponseMessage(ModbusWriteResponseMessage msg) {
520 if (writeResult != null) {
521 writeResult.set(msg);
525 private void handleDataReadOutMessage(ModbusDataReadOutMessage msg) {
526 boolean parse = true;
528 logger.debug("Received data read out message");
529 if (configuration.throttleTime > 0) {
530 if ((lastUpdateTime + configuration.throttleTime) > System.currentTimeMillis()) {
531 logger.debug("Skipping data read out message parsing");
537 logger.debug("Parsing data read out message");
538 lastUpdateTime = System.currentTimeMillis();
539 List<ModbusValue> regValues = msg.getValues();
541 if (regValues != null) {
542 for (ModbusValue val : regValues) {
543 handleVariableUpdate(pumpModel, val);
549 private void handleVariableUpdate(PumpModel pumpModel, ModbusValue value) {
550 logger.debug("Received variable update: {}", value);
551 int coilAddress = value.getCoilAddress();
553 VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
555 if (variableInfo != null) {
556 logger.trace("Using variable information to register {}: {}", coilAddress, variableInfo);
558 int val = value.getValue();
559 logger.debug("{} = {}", coilAddress + ":" + variableInfo.variable + "/" + variableInfo.factor, val);
561 CacheObject oldValue = stateMap.get(coilAddress);
563 if (oldValue != null && val == oldValue.value
564 && (oldValue.lastUpdateTime + refreshIntervalMillis() / 2) >= System.currentTimeMillis()) {
565 logger.trace("Value did not change, ignoring update");
567 final String channelPrefix = (variableInfo.type == Type.SETTING ? "setting#" : "sensor#");
568 final String channelId = channelPrefix + String.valueOf(coilAddress);
569 final String acceptedItemType = thing.getChannel(channelId).getAcceptedItemType();
571 logger.trace("AcceptedItemType for channel {} = {}", channelId, acceptedItemType);
572 State state = convertNibeValueToState(variableInfo, val, acceptedItemType);
573 logger.debug("Setting state {} = {}", coilAddress + ":" + variableInfo.variable, state);
574 stateMap.put(coilAddress, new CacheObject(System.currentTimeMillis(), val));
575 updateState(new ChannelUID(getThing().getUID(), channelId), state);
578 logger.debug("Unknown register {}", coilAddress);
582 protected class CacheObject {
584 /** Time when cache object updated in milliseconds */
585 final long lastUpdateTime;
591 * Initialize cache object.
593 * @param lastUpdateTime Time in milliseconds.
594 * @param value Cache value.
596 CacheObject(long lastUpdateTime, int value) {
597 this.lastUpdateTime = lastUpdateTime;