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.regoheatpump.internal.handler;
15 import static org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants.*;
17 import java.io.BufferedReader;
18 import java.io.EOFException;
19 import java.io.IOException;
20 import java.io.InputStreamReader;
21 import java.io.OutputStream;
22 import java.net.SocketTimeoutException;
23 import java.util.Collections;
24 import java.util.HashMap;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.unit.MetricPrefix;
35 import org.openhab.core.library.unit.Units;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link HusdataHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Boris Krivonog - Initial contribution
52 abstract class HusdataHandler extends BaseThingHandler {
54 private static final Map<Integer, String> MAPPINGS;
55 private final Logger logger = LoggerFactory.getLogger(HusdataHandler.class);
56 private @Nullable RegoConnection connection;
57 private @Nullable ScheduledFuture<?> scheduledRefreshFuture;
58 private @Nullable BufferedReader bufferedReader;
61 MAPPINGS = mappings();
64 protected HusdataHandler(Thing thing) {
68 protected abstract RegoConnection createConnection() throws IOException;
71 public void initialize() {
72 bufferedReader = null;
75 connection = createConnection();
76 } catch (IOException e) {
77 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
81 updateStatus(ThingStatus.UNKNOWN);
83 scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::handleDataFromHusdataInterface, 2, 1,
88 public void dispose() {
91 ScheduledFuture<?> scheduledRefreshFuture = this.scheduledRefreshFuture;
92 this.scheduledRefreshFuture = null;
93 if (scheduledRefreshFuture != null) {
94 scheduledRefreshFuture.cancel(true);
97 RegoConnection connection = this.connection;
98 this.connection = null;
99 if (connection != null) {
105 public void handleCommand(ChannelUID channelUID, Command command) {
108 private synchronized void handleDataFromHusdataInterface() {
109 RegoConnection connection = this.connection;
110 if (connection == null) {
114 while (!Thread.interrupted()) {
116 if (!connection.isConnected()) {
117 bufferedReader = null;
118 connection.connect();
120 // Request real-time registers.
121 logger.debug("Requesting read and dump of real-time registers.");
122 final OutputStream outputStream = connection.outputStream();
123 outputStream.write(new String("XR\r\n").getBytes());
124 outputStream.flush();
127 BufferedReader bufferedReader = this.bufferedReader;
128 if (bufferedReader == null) {
129 bufferedReader = new BufferedReader(new InputStreamReader(connection.inputStream()));
130 this.bufferedReader = bufferedReader;
133 final String line = bufferedReader.readLine();
135 throw new EOFException();
138 if (line.isEmpty()) {
142 logger.debug("Got '{}'", line);
144 processReceivedData(line);
145 } catch (SocketTimeoutException e) {
146 // Do nothing. Just happen to allow the thread to check if it has to stop.
148 } catch (IOException e) {
149 logger.warn("Processing request failed", e);
151 bufferedReader = null;
153 if (!Thread.interrupted()) {
155 updateStatus(ThingStatus.OFFLINE);
159 } catch (Exception e) {
160 logger.warn("Error occurred during message waiting", e);
165 // If state here is still unknown, than something went wrong so set thing status to OFFLINE.
166 if (thing.getStatus() == ThingStatus.UNKNOWN) {
167 updateStatus(ThingStatus.OFFLINE);
171 private void processReceivedData(final String line) {
172 if (line.length() != 10) {
173 logger.debug("Unexpected length for '{}'", line);
177 if (line.charAt(0) != 'X' || line.charAt(1) != 'R') {
178 logger.debug("Expecting XRxxxxxxxx but got '{}'", line);
182 int dataType = Integer.parseInt(line.substring(2, 3), 16);
183 int register = Integer.parseInt(line.substring(3, 6), 16);
184 int value = (short) (Integer.parseInt(line.substring(6, 8), 16) * 256
185 + Integer.parseInt(line.substring(8, 10), 16));
187 logger.debug("dataType = {}, register = {}, value = {}", dataType, register, value);
189 updateStatus(ThingStatus.ONLINE);
191 int channel = ((dataType & 0x0f) << 12) | (register & 0x0fff);
192 String channelID = MAPPINGS.get(channel);
193 if (channelID == null) {
194 logger.debug("Unsupported register {}.", register);
198 if (!isLinked(channelID)) {
199 logger.debug("Ignoring channel {} since it is not linked.", channelID);
204 case 0x00: // Degrees
205 updateState(channelID, new QuantityType<>(value / 10.0, Units.DEGREE_ANGLE));
209 updateState(channelID, new DecimalType(value / 10.0));
212 case 0x03: // Percent
213 updateState(channelID, new QuantityType<>(value / 10.0, Units.PERCENT));
217 updateState(channelID, new QuantityType<>(value / 10.0, Units.AMPERE));
221 updateState(channelID, new QuantityType<>(value / 10.0, Units.KILOWATT_HOUR));
225 updateState(channelID, new QuantityType<>(value, Units.HOUR));
228 case 0x07: // Minutes
229 updateState(channelID, new QuantityType<>(value, Units.MINUTE));
233 updateState(channelID, new QuantityType<>(value, MetricPrefix.KILO(Units.WATT)));
237 case 0x08: // Degree minutes
238 case 0x0A: // Pulses (For S0 El-meter pulse counter)
239 updateState(channelID, new DecimalType(value));
243 logger.debug("Ignoring {} due unsupported data type {}.", channelID, dataType);
248 private static Map<Integer, String> mappings() {
249 final Map<Integer, String> mappings = new HashMap<>();
252 mappings.put(0x0001, CHANNEL_GROUP_SENSOR_VALUES + "radiatorReturn");
253 mappings.put(0x0002, CHANNEL_GROUP_SENSOR_VALUES + "radiatorForward");
254 mappings.put(0x0003, CHANNEL_GROUP_SENSOR_VALUES + "heatFluidIn");
255 mappings.put(0x0004, CHANNEL_GROUP_SENSOR_VALUES + "heatFluidOut");
256 mappings.put(0x0005, CHANNEL_GROUP_SENSOR_VALUES + "coldFluidIn");
257 mappings.put(0x0006, CHANNEL_GROUP_SENSOR_VALUES + "coldFluidOut");
258 mappings.put(0x0007, CHANNEL_GROUP_SENSOR_VALUES + "outdoor");
259 mappings.put(0x0008, CHANNEL_GROUP_SENSOR_VALUES + "indoor");
260 mappings.put(0x0009, CHANNEL_GROUP_SENSOR_VALUES + "hotWater");
261 mappings.put(0x000A, CHANNEL_GROUP_SENSOR_VALUES + "externalHotWater");
262 mappings.put(0x000B, CHANNEL_GROUP_SENSOR_VALUES + "compressor");
263 mappings.put(0x000E, CHANNEL_GROUP_SENSOR_VALUES + "airIntake");
264 mappings.put(0x0011, CHANNEL_GROUP_SENSOR_VALUES + "pool");
267 mappings.put(0x3104, CHANNEL_GROUP_CONTROL_DATA + "addHeatPowerPercent"); // %
268 mappings.put(0x5104, CHANNEL_GROUP_CONTROL_DATA + "addHeatPowerEnergy"); // kWh
269 mappings.put(0x0107, CHANNEL_GROUP_CONTROL_DATA + "radiatorReturnTarget");
270 mappings.put(0x2108, CHANNEL_GROUP_CONTROL_DATA + "compressorSpeed");
273 mappings.put(0x1A01, CHANNEL_GROUP_DEVICE_VALUES + "compressor");
274 mappings.put(0x1A04, CHANNEL_GROUP_DEVICE_VALUES + "coldFluidPump");
275 mappings.put(0x1A05, CHANNEL_GROUP_DEVICE_VALUES + "heatFluidPump");
276 mappings.put(0x1A06, CHANNEL_GROUP_DEVICE_VALUES + "radiatorPump");
277 mappings.put(0x1A07, CHANNEL_GROUP_DEVICE_VALUES + "switchValve");
278 mappings.put(0x1A08, CHANNEL_GROUP_DEVICE_VALUES + "switchValve2");
279 mappings.put(0x1A09, CHANNEL_GROUP_DEVICE_VALUES + "fan");
280 mappings.put(0x1A0A, CHANNEL_GROUP_DEVICE_VALUES + "highPressostat");
281 mappings.put(0x1A0B, CHANNEL_GROUP_DEVICE_VALUES + "lowPressostat");
282 mappings.put(0x1A0C, CHANNEL_GROUP_DEVICE_VALUES + "heatingCable");
283 mappings.put(0x1A0D, CHANNEL_GROUP_DEVICE_VALUES + "crankCaseHeater");
284 mappings.put(0x1A20, CHANNEL_GROUP_DEVICE_VALUES + "alarm");
285 mappings.put(0xAFF1, CHANNEL_GROUP_DEVICE_VALUES + "elMeter1");
286 mappings.put(0xAFF2, CHANNEL_GROUP_DEVICE_VALUES + "elMeter2");
289 mappings.put(0x0203, CHANNEL_GROUP_SETTINGS + "indoorTempSetting");
290 mappings.put(0x2204, CHANNEL_GROUP_SETTINGS + "curveInflByInTemp");
291 mappings.put(0x0205, CHANNEL_GROUP_SETTINGS + "heatCurve");
292 mappings.put(0x0206, CHANNEL_GROUP_SETTINGS + "heatCurve2");
293 mappings.put(0x0207, CHANNEL_GROUP_SETTINGS + "heatCurveFineAdj");
296 return Collections.unmodifiableMap(mappings);