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