]> git.basschouten.com Git - openhab-addons.git/blob
afa1f9311b816bb6d5bd94220c0685467c2e271c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.meteostick.internal.handler;
14
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.*;
18
19 import java.io.IOException;
20 import java.math.BigDecimal;
21 import java.math.RoundingMode;
22 import java.util.Date;
23 import java.util.Set;
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;
29
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;
48
49 /**
50  * The {@link MeteostickBridgeHandler} is responsible for handling commands, which are
51  * sent to one of the channels.
52  *
53  * @author Chris Jackson - Initial contribution
54  */
55 public class MeteostickBridgeHandler extends BaseBridgeHandler {
56     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
57
58     private final Logger logger = LoggerFactory.getLogger(MeteostickBridgeHandler.class);
59
60     private static final int RECEIVE_TIMEOUT = 3000;
61
62     private SerialPort serialPort;
63     private final SerialPortManager serialPortManager;
64     private ReceiveThread receiveThread;
65
66     private ScheduledFuture<?> offlineTimerJob;
67
68     private String meteostickMode = "m1";
69     private final String meteostickFormat = "o1";
70
71     private Date lastData;
72
73     private ConcurrentMap<Integer, MeteostickEventListener> eventListeners = new ConcurrentHashMap<>();
74
75     public MeteostickBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
76         super(thing);
77         this.serialPortManager = serialPortManager;
78     }
79
80     @Override
81     public void initialize() {
82         logger.debug("Initializing MeteoStick Bridge handler.");
83
84         updateStatus(ThingStatus.UNKNOWN);
85
86         Configuration config = getThing().getConfiguration();
87
88         final String port = (String) config.get("port");
89
90         final BigDecimal mode = (BigDecimal) config.get("mode");
91         if (mode != null) {
92             meteostickMode = "m" + mode.toString();
93         }
94
95         Runnable pollingRunnable = () -> {
96             if (connectPort(port)) {
97                 offlineTimerJob.cancel(true);
98             }
99         };
100
101         // Scheduling a job on each hour to update the last hour rainfall
102         offlineTimerJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 60, TimeUnit.SECONDS);
103     }
104
105     @Override
106     public void dispose() {
107         disconnect();
108         if (offlineTimerJob != null) {
109             offlineTimerJob.cancel(true);
110         }
111     }
112
113     @Override
114     public void handleCommand(ChannelUID channelUID, Command command) {
115     }
116
117     private void resetMeteoStick() {
118         sendToMeteostick("r");
119     }
120
121     protected void subscribeEvents(int channel, MeteostickEventListener handler) {
122         logger.debug("MeteoStick bridge: subscribeEvents to channel {} with {}", channel, handler);
123
124         if (eventListeners.containsKey(channel)) {
125             logger.debug("MeteoStick bridge: subscribeEvents to channel {} already registered", channel);
126             return;
127         }
128         eventListeners.put(channel, handler);
129
130         resetMeteoStick();
131     }
132
133     protected void unsubscribeEvents(int channel, MeteostickEventListener handler) {
134         logger.debug("MeteoStick bridge: unsubscribeEvents to channel {} with {}", channel, handler);
135
136         eventListeners.remove(channel, handler);
137
138         resetMeteoStick();
139     }
140
141     /**
142      * Connects to the comm port and starts send and receive threads.
143      *
144      * @param serialPortName the port name to open
145      * @throws SerialInterfaceException when a connection error occurs.
146      */
147     private boolean connectPort(final String serialPortName) {
148         logger.debug("MeteoStick Connecting to serial port {}", serialPortName);
149
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");
154             return false;
155         }
156
157         boolean success = false;
158         try {
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);
164
165             receiveThread = new ReceiveThread();
166             receiveThread.start();
167
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);
172
173             logger.debug("Serial port is initialized");
174
175             success = true;
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);
185         }
186
187         return success;
188     }
189
190     /**
191      * Disconnects from the serial interface and stops send and receive threads.
192      */
193     private void disconnect() {
194         if (receiveThread != null) {
195             receiveThread.interrupt();
196             try {
197                 receiveThread.join();
198             } catch (InterruptedException e) {
199             }
200             receiveThread = null;
201         }
202
203         if (this.serialPort != null) {
204             this.serialPort.close();
205             this.serialPort = null;
206         }
207         logger.debug("Disconnected from serial port");
208     }
209
210     private void sendToMeteostick(String string) {
211         try {
212             synchronized (serialPort.getOutputStream()) {
213                 serialPort.getOutputStream().write(string.getBytes());
214                 serialPort.getOutputStream().write(13);
215                 serialPort.getOutputStream().flush();
216             }
217         } catch (IOException e) {
218             logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
219         }
220     }
221
222     private class ReceiveThread extends Thread implements SerialPortEventListener {
223         private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
224
225         @Override
226         public void serialEvent(SerialPortEvent arg0) {
227             try {
228                 logger.trace("RXTX library CPU load workaround, sleep forever");
229                 Thread.sleep(Long.MAX_VALUE);
230             } catch (InterruptedException e) {
231             }
232         }
233
234         /**
235          * Run method. Runs the actual receiving process.
236          */
237         @Override
238         public void run() {
239             logger.debug("Starting MeteoStick Receive Thread");
240             byte[] rxPacket = new byte[100];
241             int rxCnt = 0;
242             int rxByte;
243             while (!interrupted()) {
244                 try {
245                     rxByte = serialPort.getInputStream().read();
246
247                     if (rxByte == -1) {
248                         continue;
249                     }
250
251                     lastData = new Date();
252                     startTimeoutCheck();
253
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+");
259
260                         switch (p[0]) {
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));
265
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)));
269                                 break;
270                             case "#":
271                                 break;
272                             case "?":
273                                 // Create the channel command
274                                 int channels = 0;
275                                 for (int channel : eventListeners.keySet()) {
276                                     channels += Math.pow(2, channel - 1);
277                                 }
278
279                                 // Device has been reset - reconfigure
280                                 sendToMeteostick(meteostickFormat);
281                                 sendToMeteostick(meteostickMode);
282                                 sendToMeteostick("t" + channels);
283                                 break;
284                             default:
285                                 if (p.length < 3) {
286                                     logger.debug("MeteoStick bridge: short data ({})", p.length);
287                                     break;
288                                 }
289
290                                 try {
291                                     MeteostickEventListener listener = eventListeners.get(Integer.parseInt(p[1]));
292                                     if (listener != null) {
293                                         listener.onDataReceived(p);
294                                     } else {
295                                         logger.debug("MeteoStick bridge: data from channel {} with no handler",
296                                                 Integer.parseInt(p[1]));
297                                     }
298                                 } catch (NumberFormatException e) {
299                                 }
300                                 break;
301                         }
302
303                         updateStatus(ThingStatus.ONLINE);
304
305                         rxCnt = 0;
306                     } else if (rxByte != 10) {
307                         // Ignore line feed
308                         rxPacket[rxCnt] = (byte) rxByte;
309
310                         if (rxCnt < rxPacket.length) {
311                             rxCnt++;
312                         }
313                     }
314                 } catch (Exception e) {
315                     rxCnt = 0;
316                     logger.error("Exception during MeteoStick receive thread", e);
317                 }
318             }
319
320             logger.debug("Stopping MeteoStick Receive Thread");
321             serialPort.removeEventListener();
322         }
323     }
324
325     private synchronized void startTimeoutCheck() {
326         Runnable pollingRunnable = () -> {
327             String detail;
328             if (lastData == null) {
329                 detail = "No data received";
330             } else {
331                 detail = "No data received since " + lastData.toString();
332             }
333             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
334         };
335
336         if (offlineTimerJob != null) {
337             offlineTimerJob.cancel(true);
338         }
339
340         // Scheduling a job on each hour to update the last hour rainfall
341         offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);
342     }
343 }