2 * Copyright (c) 2010-2022 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.openthermgateway.internal;
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.PrintWriter;
19 import java.net.InetSocketAddress;
20 import java.net.Socket;
21 import java.util.AbstractMap;
23 import java.util.Map.Entry;
24 import java.util.concurrent.ConcurrentHashMap;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.types.State;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * The {@link OpenThermGatewaySocketConnector} is responsible for handling the socket connection
37 * @author Arjen Korevaar - Initial contribution
38 * @author Arjan Mels - Improved robustness by re-sending commands, handling all message types (not only Boiler)
41 public class OpenThermGatewaySocketConnector implements OpenThermGatewayConnector {
42 private static final int COMMAND_RESPONSE_TIME_MILLISECONDS = 100;
43 private static final int COMMAND_TIMEOUT_MILLISECONDS = 5000;
45 private final Logger logger = LoggerFactory.getLogger(OpenThermGatewaySocketConnector.class);
47 private final OpenThermGatewayCallback callback;
48 private final String ipaddress;
49 private final int port;
51 private @Nullable PrintWriter writer;
53 private volatile boolean stopping;
54 private boolean connected;
56 private Map<String, Entry<Long, GatewayCommand>> pendingCommands = new ConcurrentHashMap<>();
58 public OpenThermGatewaySocketConnector(OpenThermGatewayCallback callback, String ipaddress, int port) {
59 this.callback = callback;
60 this.ipaddress = ipaddress;
69 logger.debug("Connecting OpenThermGatewaySocketConnector to {}:{}", this.ipaddress, this.port);
71 callback.connecting();
73 try (Socket socket = new Socket()) {
74 socket.connect(new InetSocketAddress(this.ipaddress, this.port), COMMAND_TIMEOUT_MILLISECONDS);
75 socket.setSoTimeout(COMMAND_TIMEOUT_MILLISECONDS);
81 logger.debug("OpenThermGatewaySocketConnector connected");
83 try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
84 PrintWriter wrt = new PrintWriter(socket.getOutputStream(), true)) {
85 // Make writer accessible on class level
88 sendCommand(GatewayCommand.parse(GatewayCommandCode.PrintReport, "A"));
89 // Set the OTGW to report every message it receives and transmits
90 sendCommand(GatewayCommand.parse(GatewayCommandCode.PrintSummary, "0"));
92 while (!stopping && !Thread.currentThread().isInterrupted()) {
94 String message = reader.readLine();
96 if (message != null) {
97 handleMessage(message);
99 logger.debug("Connection closed by OpenTherm Gateway");
104 logger.debug("Stopping OpenThermGatewaySocketConnector");
108 logger.debug("OpenThermGatewaySocketConnector disconnected");
109 callback.disconnected();
111 } catch (IOException ex) {
112 logger.warn("Unable to connect to the OpenTherm Gateway.", ex);
113 callback.disconnected();
119 logger.debug("Stopping OpenThermGatewaySocketConnector");
124 public boolean isConnected() {
129 public void sendCommand(GatewayCommand command) {
131 PrintWriter wrtr = writer;
133 String msg = command.toFullString();
135 pendingCommands.put(command.getCode(),
136 new AbstractMap.SimpleImmutableEntry<>(System.currentTimeMillis(), command));
139 logger.debug("Sending message: {}", msg);
141 wrtr.print(msg + "\r\n");
145 logger.debug("Unable to send message: {}. OpenThermGatewaySocketConnector is not connected.", msg);
149 private void handleMessage(String message) {
150 if (message.length() > 2 && message.charAt(2) == ':') {
151 String code = message.substring(0, 2);
152 String value = message.substring(3);
154 logger.debug("Received command confirmation: {}: {}", code, value);
155 pendingCommands.remove(code);
159 long currentTime = System.currentTimeMillis();
161 for (Entry<Long, GatewayCommand> timeAndCommand : pendingCommands.values()) {
162 long responseTime = timeAndCommand.getKey() + COMMAND_RESPONSE_TIME_MILLISECONDS;
163 long timeoutTime = timeAndCommand.getKey() + COMMAND_TIMEOUT_MILLISECONDS;
165 if (currentTime > responseTime && currentTime <= timeoutTime) {
166 logger.debug("Resending command: {}", timeAndCommand.getValue());
167 sendCommand(timeAndCommand.getValue());
168 } else if (currentTime > timeoutTime) {
169 pendingCommands.remove(timeAndCommand.getValue().getCode());
173 Message msg = Message.parse(message);
176 logger.trace("Received message: {}, (unknown)", message);
179 logger.trace("Received message: {}, {} {} {}", message, msg.getID(), msg.getCode(), msg.getMessageType());
182 if (DataItemGroup.dataItemGroups.containsKey(msg.getID())) {
183 DataItem[] dataItems = DataItemGroup.dataItemGroups.get(msg.getID());
185 for (DataItem dataItem : dataItems) {
188 switch (dataItem.getDataType()) {
190 state = OnOffType.from(msg.getBit(dataItem.getByteType(), dataItem.getBitPos()));
194 state = new DecimalType(msg.getUInt(dataItem.getByteType()));
198 state = new DecimalType(msg.getInt(dataItem.getByteType()));
201 state = new DecimalType(msg.getFloat());
207 logger.trace(" Data: {} {} {} {}", dataItem.getID(), dataItem.getSubject(), dataItem.getDataType(),
208 state == null ? "" : state);
212 if (msg.getMessageType() == MessageType.READACK || msg.getMessageType() == MessageType.WRITEDATA
213 || msg.getID() == 0 || msg.getID() == 1) {
218 private void receiveMessage(Message message) {
219 callback.receiveMessage(message);