]> git.basschouten.com Git - openhab-addons.git/blob
bb62e1dd7b09bd1586e93c9539bfb8d2881b01b3
[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.MILLI;
17 import static org.openhab.core.library.unit.SIUnits.*;
18 import static org.openhab.core.library.unit.Units.*;
19
20 import java.math.BigDecimal;
21 import java.math.RoundingMode;
22 import java.util.Collections;
23 import java.util.Date;
24 import java.util.Iterator;
25 import java.util.Set;
26 import java.util.SortedMap;
27 import java.util.TreeMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.ThingStatusInfo;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The {@link MeteostickSensorHandler} is responsible for handling commands, which are
47  * sent to one of the channels.
48  *
49  * @author Chris Jackson - Initial contribution
50  * @author John Cocula - Added variable spoon size, UoM, wind stats, bug fixes
51  */
52 public class MeteostickSensorHandler extends BaseThingHandler implements MeteostickEventListener {
53     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DAVIS);
54
55     private final Logger logger = LoggerFactory.getLogger(MeteostickSensorHandler.class);
56
57     private int channel = 0;
58     private BigDecimal spoon = new BigDecimal(PARAMETER_SPOON_DEFAULT);
59     private MeteostickBridgeHandler bridgeHandler;
60     private RainHistory rainHistory = new RainHistory(HOUR_IN_MSEC);
61     private WindHistory windHistory = new WindHistory(2 * 60 * 1000); // 2 minutes
62     private ScheduledFuture<?> rainHourlyJob;
63     private ScheduledFuture<?> wind2MinJob;
64     private ScheduledFuture<?> offlineTimerJob;
65
66     private Date lastData;
67
68     public MeteostickSensorHandler(Thing thing) {
69         super(thing);
70     }
71
72     @Override
73     public void initialize() {
74         logger.debug("Initializing MeteoStick handler.");
75
76         channel = ((BigDecimal) getConfig().get(PARAMETER_CHANNEL)).intValue();
77
78         spoon = (BigDecimal) getConfig().get(PARAMETER_SPOON);
79         if (spoon == null) {
80             spoon = new BigDecimal(PARAMETER_SPOON_DEFAULT);
81         }
82         logger.debug("Initializing MeteoStick handler - Channel {}, Spoon size {} mm.", channel, spoon);
83
84         Runnable rainRunnable = () -> {
85             BigDecimal rainfall = rainHistory.getTotal(spoon);
86             rainfall.setScale(1, RoundingMode.DOWN);
87             updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_LASTHOUR),
88                     new QuantityType<>(rainfall, MILLI(METRE)));
89         };
90
91         // Scheduling a job on each hour to update the last hour rainfall
92         long start = HOUR_IN_SEC - ((System.currentTimeMillis() % HOUR_IN_MSEC) / 1000);
93         rainHourlyJob = scheduler.scheduleWithFixedDelay(rainRunnable, start, HOUR_IN_SEC, TimeUnit.SECONDS);
94
95         Runnable windRunnable = () -> {
96             WindStats stats = windHistory.getStats();
97             updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED_LAST2MIN_AVERAGE),
98                     new QuantityType<>(stats.averageSpeed, METRE_PER_SECOND));
99             updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED_LAST2MIN_MAXIMUM),
100                     new QuantityType<>(stats.maxSpeed, METRE_PER_SECOND));
101             updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_DIRECTION_LAST2MIN_AVERAGE),
102                     new QuantityType<>(stats.averageDirection, DEGREE_ANGLE));
103         };
104
105         // Scheduling a job to run every two minutes to update wind statistics
106         wind2MinJob = scheduler.scheduleWithFixedDelay(windRunnable, 2, 2, TimeUnit.MINUTES);
107
108         updateStatus(ThingStatus.UNKNOWN);
109     }
110
111     @Override
112     public void dispose() {
113         if (rainHourlyJob != null) {
114             rainHourlyJob.cancel(true);
115         }
116
117         if (wind2MinJob != null) {
118             wind2MinJob.cancel(true);
119         }
120
121         if (offlineTimerJob != null) {
122             offlineTimerJob.cancel(true);
123         }
124
125         if (bridgeHandler != null) {
126             bridgeHandler.unsubscribeEvents(channel, this);
127         }
128     }
129
130     @Override
131     public void handleCommand(ChannelUID channelUID, Command command) {
132     }
133
134     @Override
135     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
136         logger.debug("MeteoStick handler {}: bridgeStatusChanged to {}", channel, bridgeStatusInfo);
137         if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
138             logger.debug("MeteoStick handler {}: bridgeStatusChanged but bridge offline", channel);
139             updateStatus(ThingStatus.OFFLINE);
140             return;
141         }
142
143         bridgeHandler = (MeteostickBridgeHandler) getBridge().getHandler();
144
145         if (channel != 0) {
146             if (bridgeHandler != null) {
147                 bridgeHandler.subscribeEvents(channel, this);
148             }
149         }
150
151         // Put the thing online and start our "no data" timer
152         updateStatus(ThingStatus.ONLINE);
153         startTimeoutCheck();
154     }
155
156     private void processSignalStrength(String dbmString) {
157         double dbm = Double.parseDouble(dbmString);
158         int strength;
159
160         if (dbm > -60) {
161             strength = 4;
162         } else if (dbm > -70) {
163             strength = 3;
164         } else if (dbm > -80) {
165             strength = 2;
166         } else if (dbm > -90) {
167             strength = 1;
168         } else {
169             strength = 0;
170         }
171
172         updateState(new ChannelUID(getThing().getUID(), CHANNEL_SIGNAL_STRENGTH), new DecimalType(strength));
173     }
174
175     private void processBattery(boolean batteryLow) {
176         OnOffType state = batteryLow ? OnOffType.ON : OnOffType.OFF;
177
178         updateState(new ChannelUID(getThing().getUID(), CHANNEL_LOW_BATTERY), state);
179     }
180
181     @Override
182     public void onDataReceived(String[] data) {
183         logger.debug("MeteoStick received channel {}: {}", channel, data);
184         updateStatus(ThingStatus.ONLINE);
185         lastData = new Date();
186
187         startTimeoutCheck();
188
189         switch (data[0]) {
190             case "R": // Rain
191                 int rain = Integer.parseInt(data[2]);
192                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_RAW), new DecimalType(rain));
193                 processSignalStrength(data[3]);
194                 processBattery(data.length == 5);
195
196                 rainHistory.put(rain);
197
198                 BigDecimal rainfall = rainHistory.getTotal(spoon);
199                 rainfall.setScale(1, RoundingMode.DOWN);
200                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_CURRENTHOUR),
201                         new QuantityType<>(rainfall, MILLI(METRE)));
202                 break;
203             case "W": // Wind
204                 BigDecimal windSpeed = new BigDecimal(data[2]);
205                 int windDirection = Integer.parseInt(data[3]);
206                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED),
207                         new QuantityType<>(windSpeed, METRE_PER_SECOND));
208                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_DIRECTION),
209                         new QuantityType<>(windDirection, DEGREE_ANGLE));
210
211                 windHistory.put(windSpeed, windDirection);
212
213                 processSignalStrength(data[4]);
214                 processBattery(data.length == 6);
215                 break;
216             case "T": // Temperature
217                 BigDecimal temperature = new BigDecimal(data[2]);
218                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_OUTDOOR_TEMPERATURE),
219                         new QuantityType<>(temperature.setScale(1), CELSIUS));
220
221                 BigDecimal humidity = new BigDecimal(data[3]);
222                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_HUMIDITY),
223                         new DecimalType(humidity.setScale(1)));
224
225                 processSignalStrength(data[4]);
226                 processBattery(data.length == 6);
227                 break;
228             case "P": // Solar panel power
229                 BigDecimal power = new BigDecimal(data[2]);
230                 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SOLAR_POWER),
231                         new DecimalType(power.setScale(1)));
232
233                 processSignalStrength(data[3]);
234                 processBattery(data.length == 5);
235                 break;
236         }
237     }
238
239     class SlidingTimeWindow<T> {
240         private long period = 0;
241         protected final SortedMap<Long, T> storage = Collections.synchronizedSortedMap(new TreeMap<>());
242
243         /**
244          *
245          * @param period window period in milliseconds
246          */
247         public SlidingTimeWindow(long period) {
248             this.period = period;
249         }
250
251         public void put(T value) {
252             storage.put(System.currentTimeMillis(), value);
253         }
254
255         public void removeOldEntries() {
256             long old = System.currentTimeMillis() - period;
257             synchronized (storage) {
258                 for (Iterator<Long> iterator = storage.keySet().iterator(); iterator.hasNext();) {
259                     long time = iterator.next();
260                     if (time < old) {
261                         iterator.remove();
262                     }
263                 }
264             }
265         }
266     }
267
268     class RainHistory extends SlidingTimeWindow<Integer> {
269
270         public RainHistory(long period) {
271             super(period);
272         }
273
274         public BigDecimal getTotal(BigDecimal spoon) {
275             removeOldEntries();
276
277             int least = -1;
278             int total = 0;
279
280             synchronized (storage) {
281                 for (int value : storage.values()) {
282
283                     /*
284                      * Rain counters have been seen to wrap at 127 and also at 255.
285                      * The Meteostick documentation only mentions 255 at the time of
286                      * this writing. This potential difference is solved by having
287                      * all rain counters wrap at 127 (0x7F) by removing the high bit.
288                      */
289                     value &= 0x7F;
290
291                     if (least == -1) {
292                         least = value;
293                         continue;
294                     }
295
296                     if (value < least) {
297                         total = 128 - least + value;
298                     } else {
299                         total = value - least;
300                     }
301                 }
302             }
303
304             return BigDecimal.valueOf(total).multiply(spoon);
305         }
306     }
307
308     /**
309      * Store the wind direction as an east-west vector and a north-south vector
310      * so that an average direction can be calculated based on the wind speed
311      * at the time of the direction sample.
312      */
313     class WindSample {
314         double speed;
315         double ewVector;
316         double nsVector;
317
318         public WindSample(BigDecimal speed, int directionDegrees) {
319             this.speed = speed.doubleValue();
320             double direction = Math.toRadians(directionDegrees);
321             this.ewVector = this.speed * Math.sin(direction);
322             this.nsVector = this.speed * Math.cos(direction);
323         }
324     }
325
326     class WindStats {
327         BigDecimal averageSpeed;
328         int averageDirection;
329         BigDecimal maxSpeed;
330     }
331
332     class WindHistory extends SlidingTimeWindow<WindSample> {
333
334         public WindHistory(long period) {
335             super(period);
336         }
337
338         public void put(BigDecimal speed, int directionDegrees) {
339             put(new WindSample(speed, directionDegrees));
340         }
341
342         public WindStats getStats() {
343             removeOldEntries();
344
345             double ewSum = 0;
346             double nsSum = 0;
347             double totalSpeed = 0;
348             double maxSpeed = 0;
349             int size = 0;
350             synchronized (storage) {
351                 size = storage.size();
352                 for (WindSample sample : storage.values()) {
353                     ewSum += sample.ewVector;
354                     nsSum += sample.nsVector;
355                     totalSpeed += sample.speed;
356                     if (sample.speed > maxSpeed) {
357                         maxSpeed = sample.speed;
358                     }
359                 }
360             }
361
362             WindStats stats = new WindStats();
363
364             stats.averageDirection = (int) Math.toDegrees(Math.atan2(ewSum, nsSum));
365             if (stats.averageDirection < 0) {
366                 stats.averageDirection += 360;
367             }
368
369             stats.averageSpeed = new BigDecimal(size > 0 ? totalSpeed / size : 0).setScale(3, RoundingMode.HALF_DOWN);
370
371             stats.maxSpeed = new BigDecimal(maxSpeed).setScale(3, RoundingMode.HALF_DOWN);
372
373             return stats;
374         }
375     }
376
377     private synchronized void startTimeoutCheck() {
378         Runnable pollingRunnable = () -> {
379             String detail;
380             if (lastData == null) {
381                 detail = "No data received";
382             } else {
383                 detail = "No data received since " + lastData.toString();
384             }
385             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
386         };
387
388         if (offlineTimerJob != null) {
389             offlineTimerJob.cancel(true);
390         }
391
392         // Scheduling a job on each hour to update the last hour rainfall
393         offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);
394     }
395 }