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.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.Date;
24 import java.util.TooManyListenersException;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.ConcurrentMap;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import org.openhab.core.config.core.Configuration;
31 import org.openhab.core.io.transport.serial.PortInUseException;
32 import org.openhab.core.io.transport.serial.SerialPort;
33 import org.openhab.core.io.transport.serial.SerialPortEvent;
34 import org.openhab.core.io.transport.serial.SerialPortEventListener;
35 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
36 import org.openhab.core.io.transport.serial.SerialPortManager;
37 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link MeteostickBridgeHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Chris Jackson - Initial contribution
55 public class MeteostickBridgeHandler extends BaseBridgeHandler {
56 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
58 private final Logger logger = LoggerFactory.getLogger(MeteostickBridgeHandler.class);
60 private static final int RECEIVE_TIMEOUT = 3000;
62 private SerialPort serialPort;
63 private final SerialPortManager serialPortManager;
64 private ReceiveThread receiveThread;
66 private ScheduledFuture<?> offlineTimerJob;
68 private String meteostickMode = "m1";
69 private final String meteostickFormat = "o1";
71 private Date lastData;
73 private ConcurrentMap<Integer, MeteostickEventListener> eventListeners = new ConcurrentHashMap<>();
75 public MeteostickBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
77 this.serialPortManager = serialPortManager;
81 public void initialize() {
82 logger.debug("Initializing MeteoStick Bridge handler.");
84 updateStatus(ThingStatus.UNKNOWN);
86 Configuration config = getThing().getConfiguration();
88 final String port = (String) config.get("port");
90 final BigDecimal mode = (BigDecimal) config.get("mode");
92 meteostickMode = "m" + mode.toString();
95 Runnable pollingRunnable = () -> {
96 if (connectPort(port)) {
97 offlineTimerJob.cancel(true);
101 // Scheduling a job on each hour to update the last hour rainfall
102 offlineTimerJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 60, TimeUnit.SECONDS);
106 public void dispose() {
108 if (offlineTimerJob != null) {
109 offlineTimerJob.cancel(true);
114 public void handleCommand(ChannelUID channelUID, Command command) {
117 private void resetMeteoStick() {
118 sendToMeteostick("r");
121 protected void subscribeEvents(int channel, MeteostickEventListener handler) {
122 logger.debug("MeteoStick bridge: subscribeEvents to channel {} with {}", channel, handler);
124 if (eventListeners.containsKey(channel)) {
125 logger.debug("MeteoStick bridge: subscribeEvents to channel {} already registered", channel);
128 eventListeners.put(channel, handler);
133 protected void unsubscribeEvents(int channel, MeteostickEventListener handler) {
134 logger.debug("MeteoStick bridge: unsubscribeEvents to channel {} with {}", channel, handler);
136 eventListeners.remove(channel, handler);
142 * Connects to the comm port and starts send and receive threads.
144 * @param serialPortName the port name to open
145 * @throws SerialInterfaceException when a connection error occurs.
147 private boolean connectPort(final String serialPortName) {
148 logger.debug("MeteoStick Connecting to serial port {}", serialPortName);
150 SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
151 if (portIdentifier == null) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
153 "Serial Error: Port " + serialPortName + " does not exist");
157 boolean success = false;
159 serialPort = portIdentifier.open("org.openhab.binding.meteostick", 2000);
160 serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
161 SerialPort.PARITY_NONE);
162 serialPort.enableReceiveThreshold(1);
163 serialPort.enableReceiveTimeout(RECEIVE_TIMEOUT);
165 receiveThread = new ReceiveThread();
166 receiveThread.start();
168 // RXTX serial port library causes high CPU load
169 // Start event listener, which will just sleep and slow down event loop
170 serialPort.addEventListener(this.receiveThread);
171 serialPort.notifyOnDataAvailable(true);
173 logger.debug("Serial port is initialized");
176 } catch (PortInUseException e) {
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
178 "Serial Error: Port " + serialPortName + " in use");
179 } catch (UnsupportedCommOperationException e) {
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
181 "Serial Error: Unsupported comm operation on port " + serialPortName);
182 } catch (TooManyListenersException e) {
183 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
184 "Serial Error: Too many listeners on port " + serialPortName);
191 * Disconnects from the serial interface and stops send and receive threads.
193 private void disconnect() {
194 if (receiveThread != null) {
195 receiveThread.interrupt();
197 receiveThread.join();
198 } catch (InterruptedException e) {
200 receiveThread = null;
203 if (this.serialPort != null) {
204 this.serialPort.close();
205 this.serialPort = null;
207 logger.debug("Disconnected from serial port");
210 private void sendToMeteostick(String string) {
212 synchronized (serialPort.getOutputStream()) {
213 serialPort.getOutputStream().write(string.getBytes());
214 serialPort.getOutputStream().write(13);
215 serialPort.getOutputStream().flush();
217 } catch (IOException e) {
218 logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
222 private class ReceiveThread extends Thread implements SerialPortEventListener {
223 private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
226 public void serialEvent(SerialPortEvent arg0) {
228 logger.trace("RXTX library CPU load workaround, sleep forever");
229 Thread.sleep(Long.MAX_VALUE);
230 } catch (InterruptedException e) {
235 * Run method. Runs the actual receiving process.
239 logger.debug("Starting MeteoStick Receive Thread");
240 byte[] rxPacket = new byte[100];
243 while (!interrupted()) {
245 rxByte = serialPort.getInputStream().read();
251 lastData = new Date();
254 // Check for end of line
255 if (rxByte == 13 && rxCnt > 0) {
256 String inputString = new String(rxPacket, 0, rxCnt);
257 logger.debug("MeteoStick received: {}", inputString);
258 String[] p = inputString.split("\\s+");
261 case "B": // Barometer
262 BigDecimal temperature = new BigDecimal(p[1]);
263 updateState(new ChannelUID(getThing().getUID(), CHANNEL_INDOOR_TEMPERATURE),
264 new QuantityType<>(temperature.setScale(1), CELSIUS));
266 BigDecimal pressure = new BigDecimal(p[2]);
267 updateState(new ChannelUID(getThing().getUID(), CHANNEL_PRESSURE),
268 new QuantityType<>(pressure.setScale(1, RoundingMode.HALF_UP), HECTO(PASCAL)));
273 // Create the channel command
275 for (int channel : eventListeners.keySet()) {
276 channels += Math.pow(2, channel - 1);
279 // Device has been reset - reconfigure
280 sendToMeteostick(meteostickFormat);
281 sendToMeteostick(meteostickMode);
282 sendToMeteostick("t" + channels);
286 logger.debug("MeteoStick bridge: short data ({})", p.length);
291 MeteostickEventListener listener = eventListeners.get(Integer.parseInt(p[1]));
292 if (listener != null) {
293 listener.onDataReceived(p);
295 logger.debug("MeteoStick bridge: data from channel {} with no handler",
296 Integer.parseInt(p[1]));
298 } catch (NumberFormatException e) {
303 updateStatus(ThingStatus.ONLINE);
306 } else if (rxByte != 10) {
308 rxPacket[rxCnt] = (byte) rxByte;
310 if (rxCnt < rxPacket.length) {
314 } catch (Exception e) {
316 logger.error("Exception during MeteoStick receive thread", e);
320 logger.debug("Stopping MeteoStick Receive Thread");
321 serialPort.removeEventListener();
325 private synchronized void startTimeoutCheck() {
326 Runnable pollingRunnable = () -> {
328 if (lastData == null) {
329 detail = "No data received";
331 detail = "No data received since " + lastData.toString();
333 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
336 if (offlineTimerJob != null) {
337 offlineTimerJob.cancel(true);
340 // Scheduling a job on each hour to update the last hour rainfall
341 offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);