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.meteostick.internal.handler;
15 import static org.openhab.binding.meteostick.internal.MeteostickBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.HECTO;
17 import static org.openhab.core.library.unit.SIUnits.*;
19 import java.io.IOException;
20 import java.math.BigDecimal;
21 import java.math.RoundingMode;
22 import java.util.Collections;
23 import java.util.Date;
25 import java.util.TooManyListenersException;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ConcurrentMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
31 import org.openhab.core.config.core.Configuration;
32 import org.openhab.core.io.transport.serial.PortInUseException;
33 import org.openhab.core.io.transport.serial.SerialPort;
34 import org.openhab.core.io.transport.serial.SerialPortEvent;
35 import org.openhab.core.io.transport.serial.SerialPortEventListener;
36 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
37 import org.openhab.core.io.transport.serial.SerialPortManager;
38 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.types.Command;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * The {@link MeteostickBridgeHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author Chris Jackson - Initial contribution
56 public class MeteostickBridgeHandler extends BaseBridgeHandler {
57 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
59 private final Logger logger = LoggerFactory.getLogger(MeteostickBridgeHandler.class);
61 private static final int RECEIVE_TIMEOUT = 3000;
63 private SerialPort serialPort;
64 private final SerialPortManager serialPortManager;
65 private ReceiveThread receiveThread;
67 private ScheduledFuture<?> offlineTimerJob;
69 private String meteostickMode = "m1";
70 private final String meteostickFormat = "o1";
72 private Date lastData;
74 private ConcurrentMap<Integer, MeteostickEventListener> eventListeners = new ConcurrentHashMap<>();
76 public MeteostickBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
78 this.serialPortManager = serialPortManager;
82 public void initialize() {
83 logger.debug("Initializing MeteoStick Bridge handler.");
85 updateStatus(ThingStatus.UNKNOWN);
87 Configuration config = getThing().getConfiguration();
89 final String port = (String) config.get("port");
91 final BigDecimal mode = (BigDecimal) config.get("mode");
93 meteostickMode = "m" + mode.toString();
96 Runnable pollingRunnable = () -> {
97 if (connectPort(port)) {
98 offlineTimerJob.cancel(true);
102 // Scheduling a job on each hour to update the last hour rainfall
103 offlineTimerJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 60, TimeUnit.SECONDS);
107 public void dispose() {
109 if (offlineTimerJob != null) {
110 offlineTimerJob.cancel(true);
115 public void handleCommand(ChannelUID channelUID, Command command) {
118 private void resetMeteoStick() {
119 sendToMeteostick("r");
122 protected void subscribeEvents(int channel, MeteostickEventListener handler) {
123 logger.debug("MeteoStick bridge: subscribeEvents to channel {} with {}", channel, handler);
125 if (eventListeners.containsKey(channel)) {
126 logger.debug("MeteoStick bridge: subscribeEvents to channel {} already registered", channel);
129 eventListeners.put(channel, handler);
134 protected void unsubscribeEvents(int channel, MeteostickEventListener handler) {
135 logger.debug("MeteoStick bridge: unsubscribeEvents to channel {} with {}", channel, handler);
137 eventListeners.remove(channel, handler);
143 * Connects to the comm port and starts send and receive threads.
145 * @param serialPortName the port name to open
146 * @throws SerialInterfaceException when a connection error occurs.
148 private boolean connectPort(final String serialPortName) {
149 logger.debug("MeteoStick Connecting to serial port {}", serialPortName);
151 SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
152 if (portIdentifier == null) {
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
154 "Serial Error: Port " + serialPortName + " does not exist");
158 boolean success = false;
160 serialPort = portIdentifier.open("org.openhab.binding.meteostick", 2000);
161 serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
162 SerialPort.PARITY_NONE);
163 serialPort.enableReceiveThreshold(1);
164 serialPort.enableReceiveTimeout(RECEIVE_TIMEOUT);
166 receiveThread = new ReceiveThread();
167 receiveThread.start();
169 // RXTX serial port library causes high CPU load
170 // Start event listener, which will just sleep and slow down event loop
171 serialPort.addEventListener(this.receiveThread);
172 serialPort.notifyOnDataAvailable(true);
174 logger.debug("Serial port is initialized");
177 } catch (PortInUseException e) {
178 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
179 "Serial Error: Port " + serialPortName + " in use");
180 } catch (UnsupportedCommOperationException e) {
181 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
182 "Serial Error: Unsupported comm operation on port " + serialPortName);
183 } catch (TooManyListenersException e) {
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
185 "Serial Error: Too many listeners on port " + serialPortName);
192 * Disconnects from the serial interface and stops send and receive threads.
194 private void disconnect() {
195 if (receiveThread != null) {
196 receiveThread.interrupt();
198 receiveThread.join();
199 } catch (InterruptedException e) {
201 receiveThread = null;
204 if (this.serialPort != null) {
205 this.serialPort.close();
206 this.serialPort = null;
208 logger.debug("Disconnected from serial port");
211 private void sendToMeteostick(String string) {
213 synchronized (serialPort.getOutputStream()) {
214 serialPort.getOutputStream().write(string.getBytes());
215 serialPort.getOutputStream().write(13);
216 serialPort.getOutputStream().flush();
218 } catch (IOException e) {
219 logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
223 private class ReceiveThread extends Thread implements SerialPortEventListener {
224 private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
227 public void serialEvent(SerialPortEvent arg0) {
229 logger.trace("RXTX library CPU load workaround, sleep forever");
230 Thread.sleep(Long.MAX_VALUE);
231 } catch (InterruptedException e) {
236 * Run method. Runs the actual receiving process.
240 logger.debug("Starting MeteoStick Receive Thread");
241 byte[] rxPacket = new byte[100];
244 while (!interrupted()) {
246 rxByte = serialPort.getInputStream().read();
252 lastData = new Date();
255 // Check for end of line
256 if (rxByte == 13 && rxCnt > 0) {
257 String inputString = new String(rxPacket, 0, rxCnt);
258 logger.debug("MeteoStick received: {}", inputString);
259 String p[] = inputString.split("\\s+");
262 case "B": // Barometer
263 BigDecimal temperature = new BigDecimal(p[1]);
264 updateState(new ChannelUID(getThing().getUID(), CHANNEL_INDOOR_TEMPERATURE),
265 new QuantityType<>(temperature.setScale(1), CELSIUS));
267 BigDecimal pressure = new BigDecimal(p[2]);
268 updateState(new ChannelUID(getThing().getUID(), CHANNEL_PRESSURE),
269 new QuantityType<>(pressure.setScale(1, RoundingMode.HALF_UP), HECTO(PASCAL)));
274 // Create the channel command
276 for (int channel : eventListeners.keySet()) {
277 channels += Math.pow(2, channel - 1);
280 // Device has been reset - reconfigure
281 sendToMeteostick(meteostickFormat);
282 sendToMeteostick(meteostickMode);
283 sendToMeteostick("t" + channels);
287 logger.debug("MeteoStick bridge: short data ({})", p.length);
292 MeteostickEventListener listener = eventListeners.get(Integer.parseInt(p[1]));
293 if (listener != null) {
294 listener.onDataReceived(p);
296 logger.debug("MeteoStick bridge: data from channel {} with no handler",
297 Integer.parseInt(p[1]));
299 } catch (NumberFormatException e) {
304 updateStatus(ThingStatus.ONLINE);
307 } else if (rxByte != 10) {
309 rxPacket[rxCnt] = (byte) rxByte;
311 if (rxCnt < rxPacket.length) {
315 } catch (Exception e) {
317 logger.error("Exception during MeteoStick receive thread", e);
321 logger.debug("Stopping MeteoStick Receive Thread");
322 serialPort.removeEventListener();
326 private synchronized void startTimeoutCheck() {
327 Runnable pollingRunnable = () -> {
329 if (lastData == null) {
330 detail = "No data received";
332 detail = "No data received since " + lastData.toString();
334 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
337 if (offlineTimerJob != null) {
338 offlineTimerJob.cancel(true);
341 // Scheduling a job on each hour to update the last hour rainfall
342 offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);