2 * Copyright (c) 2010-2024 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.heliosventilation.internal;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.util.HashMap;
20 import java.util.TooManyListenersException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.io.transport.serial.PortInUseException;
27 import org.openhab.core.io.transport.serial.SerialPort;
28 import org.openhab.core.io.transport.serial.SerialPortEvent;
29 import org.openhab.core.io.transport.serial.SerialPortEventListener;
30 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
31 import org.openhab.core.io.transport.serial.SerialPortManager;
32 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
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.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link HeliosVentilationHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Raphael Mack - Initial contribution
54 public class HeliosVentilationHandler extends BaseThingHandler implements SerialPortEventListener {
55 private static final int BUSMEMBER_MAINBOARD = 0x11;
56 private static final int BUSMEMBER_SLAVEBOARDS = 0x10;
57 private static final byte BUSMEMBER_CONTROLBOARDS = (byte) 0x20;
58 private static final int BUSMEMBER_REC_MASK = 0xF0; // interpreting frames delivered to BUSMEMBER_ME &
60 private static final int BUSMEMBER_ME = 0x2F; // used as sender when communicating with the helios system
61 private static final int POLL_OFFLINE_THRESHOLD = 3;
63 /** Logger Instance */
64 private final Logger logger = LoggerFactory.getLogger(HeliosVentilationHandler.class);
67 * store received data for read-modify-write operations on bitlevel
69 private final Map<Byte, Byte> memory = new HashMap<Byte, Byte>();
71 private final SerialPortManager serialPortManager;
74 * init to default to avoid NPE in case handleCommand() is called before initialize()
76 private HeliosVentilationConfiguration config = new HeliosVentilationConfiguration();
78 private @Nullable SerialPort serialPort;
79 private @Nullable InputStream inputStream;
80 private @Nullable OutputStream outputStream;
82 private @Nullable ScheduledFuture<?> pollingTask;
83 private int pollCounter;
85 public HeliosVentilationHandler(Thing thing, final SerialPortManager serialPortManager) {
87 this.serialPortManager = serialPortManager;
91 public void initialize() {
92 config = getConfigAs(HeliosVentilationConfiguration.class);
94 logger.debug("Serial Port: {}, 9600 baud, PollPeriod: {}", config.serialPort, config.pollPeriod);
96 if (config.serialPort.length() < 1) {
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
100 SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort);
101 if (portId == null) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
103 "Port " + config.serialPort + " is not known!");
106 updateStatus(ThingStatus.UNKNOWN);
107 if (this.config.pollPeriod > 0) {
113 scheduler.execute(this::connect);
116 private synchronized void connect() {
117 logger.debug("HeliosVentilation: connecting...");
118 // parse ports and if the port is found, initialize the reader
119 SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort);
120 if (portId == null) {
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
122 "Port " + config.serialPort + " is not known!");
126 } else if (!isConnected()) {
127 // initialize serial port
129 SerialPort serial = portId.open(getThing().getUID().toString(), 2000);
130 serial.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
131 serial.addEventListener(this);
134 if (inputStream != null) {
137 } catch (IOException e) {
138 // ignore the exception on close
142 if (outputStream != null) {
143 outputStream.close();
145 } catch (IOException e) {
146 // ignore the exception on close
150 inputStream = serial.getInputStream();
151 outputStream = serial.getOutputStream();
153 // activate the DATA_AVAILABLE notifier
154 serial.notifyOnDataAvailable(true);
156 updateStatus(ThingStatus.UNKNOWN);
157 } catch (final IOException ex) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
159 } catch (PortInUseException e) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
161 } catch (TooManyListenersException e) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
163 "Cannot attach listener to port!");
164 } catch (UnsupportedCommOperationException e) {
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
166 "Serial port does not support the RS485 parameters of the Helios remote protocol.");
172 public void dispose() {
179 * Start the polling task.
181 public synchronized void startPolling() {
182 final ScheduledFuture<?> task = pollingTask;
183 if (task != null && task.isCancelled()) {
186 if (config.pollPeriod > 0) {
187 pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 10, config.pollPeriod, TimeUnit.SECONDS);
194 * Stop the polling task.
196 public synchronized void stopPolling() {
197 final ScheduledFuture<?> task = pollingTask;
198 if (task != null && !task.isCancelled()) {
205 * Method for polling the RS485 Helios RemoteContol bus
207 public synchronized void polling() {
208 if (logger.isTraceEnabled()) {
209 logger.trace("HeliosVentilation Polling data for '{}'", getThing().getUID());
212 if (pollCounter > POLL_OFFLINE_THRESHOLD) {
213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE, "No data received!");
214 logger.info("No data received for '{}' disconnecting now...", getThing().getUID());
218 if (!isConnected()) {
219 connect(); // let's try to reconnect if the connection failed or was never established before
222 HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((v) -> {
223 if (isLinked(v.getName())) {
229 private void disconnect() {
230 if (thing.getStatus() != ThingStatus.REMOVING) {
231 updateStatus(ThingStatus.OFFLINE);
233 synchronized (this) {
235 if (inputStream != null) {
238 } catch (IOException e) {
239 // ignore the exception on close
243 if (outputStream != null) {
244 outputStream.close();
246 } catch (IOException e) {
247 // ignore the exception on close
251 SerialPort serial = serialPort;
252 if (serial != null) {
259 private void poll(HeliosVentilationDataPoint v) {
260 byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_MAINBOARD, 0x00, v.address(), 0x00 };
261 txFrame[5] = (byte) checksum(txFrame);
269 private void tx(byte[] txFrame) {
271 OutputStream out = outputStream;
273 if (logger.isTraceEnabled()) {
274 logger.trace("HeliosVentilation: Write to serial port: {}",
275 String.format("%02x %02x %02x %02x", txFrame[1], txFrame[2], txFrame[3], txFrame[4]));
280 // after each frame we have to wait.
281 // 30 ms is taken from what we roughly see the original remote control is doing
284 } catch (IOException e) {
285 // in case we cannot write the connection is somehow broken, let's officially disconnect
288 } catch (InterruptedException e) {
289 // ignore if we got interrupted
294 * Check connection status
296 * @return true if currently connected
298 private boolean isConnected() {
299 return serialPort != null && inputStream != null && outputStream != null;
303 public synchronized void serialEvent(SerialPortEvent event) {
304 switch (event.getEventType()) {
305 case SerialPortEvent.DATA_AVAILABLE:
306 // we get here if data has been received
309 // Wait roughly a frame length to ensure that the complete frame is already buffered. This improves
310 // the robustness for RS485/USB converters which sometimes duplicate bytes otherwise.
312 } catch (InterruptedException e) {
313 // ignore interruption
316 byte[] frame = { 0, 0, 0, 0, 0, 0 };
317 InputStream in = inputStream;
322 // read data from serial device
323 while (cnt < 6 && in.available() > 0) {
324 final int bytes = in.read(frame, cnt, 1);
325 if (cnt > 0 || frame[0] == 0x01) {
326 // only proceed if the first byte was 0x01
330 int sum = checksum(frame);
331 if (sum == (frame[5] & 0xff)) {
332 if (logger.isTraceEnabled()) {
333 logger.trace("HeliosVentilation: Read from serial port: {}", String
334 .format("%02x %02x %02x %02x", frame[1], frame[2], frame[3], frame[4]));
336 interpretFrame(frame);
339 if (logger.isTraceEnabled()) {
341 "HeliosVentilation: Read frame with not matching checksum from serial port: {}",
342 String.format("%02x %02x %02x %02x %02x %02x (expected %02x)", frame[0],
343 frame[1], frame[2], frame[3], frame[4], frame[5], sum));
348 } while (in.available() > 0);
350 } catch (IOException e1) {
351 logger.debug("Error reading from serial port: {}", e1.getMessage(), e1);
361 public void handleCommand(ChannelUID channelUID, Command command) {
362 if (command instanceof RefreshType) {
363 scheduler.execute(this::polling);
364 } else if (command instanceof DecimalType || command instanceof QuantityType || command instanceof OnOffType) {
365 scheduler.execute(() -> update(channelUID, command));
370 * Update the variable corresponding to given channel/command
372 * @param channelUID UID of the channel to update
373 * @param command data element to write
376 public void update(ChannelUID channelUID, Command command) {
377 HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((outer) -> {
378 HeliosVentilationDataPoint v = outer;
380 if (channelUID.getThingUID().equals(thing.getUID()) && v.getName().equals(channelUID.getId())) {
381 if (v.isWritable()) {
382 byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_CONTROLBOARDS, v.address(), 0x00, 0x00 };
383 txFrame[4] = v.getTransmitDataFor((State) command);
384 if (v.requiresReadModifyWrite()) {
385 txFrame[4] |= memory.get(v.address()) & ~v.bitMask();
386 memory.put(v.address(), txFrame[4]);
388 txFrame[5] = (byte) checksum(txFrame);
391 txFrame[2] = BUSMEMBER_SLAVEBOARDS;
392 txFrame[5] = (byte) checksum(txFrame);
395 txFrame[2] = BUSMEMBER_MAINBOARD;
396 txFrame[5] = (byte) checksum(txFrame);
406 * calculate checksum of a frame
408 * @param frame filled with 5 bytes
409 * @return checksum of the first 5 bytes of frame
411 private int checksum(byte[] frame) {
413 for (int a = 0; a < 5; a++) {
414 sum += frame[a] & 0xff;
421 * interpret a frame, which is already validated to be in correct format with valid checksum
423 * @param frame 6 bytes long data with 0x01, sender, receiver, address, value, checksum
425 private void interpretFrame(byte[] frame) {
426 if ((frame[2] & BUSMEMBER_REC_MASK) == (BUSMEMBER_ME & BUSMEMBER_REC_MASK)) {
427 // something to read for us
430 if (HeliosVentilationBindingConstants.DATAPOINTS.containsKey(var)) {
431 HeliosVentilationDataPoint datapoint = HeliosVentilationBindingConstants.DATAPOINTS.get(var);
432 if (datapoint.requiresReadModifyWrite()) {
433 memory.put(var, val);
436 if (logger.isTraceEnabled()) {
437 String t = datapoint.asString(val);
438 logger.trace("Received {} = {}", datapoint, t);
440 updateStatus(ThingStatus.ONLINE);
443 updateState(datapoint.getName(), datapoint.asState(val));
444 datapoint = datapoint.next();
445 } while (datapoint != null);
448 if (logger.isTraceEnabled()) {
449 logger.trace("Received unkown data @{} = {}", String.format("%02X ", var),
450 String.format("%02X ", val));