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.MILLI;
17 import static org.openhab.core.library.unit.SIUnits.*;
18 import static org.openhab.core.library.unit.Units.*;
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;
26 import java.util.SortedMap;
27 import java.util.TreeMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
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;
46 * The {@link MeteostickSensorHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Chris Jackson - Initial contribution
50 * @author John Cocula - Added variable spoon size, UoM, wind stats, bug fixes
52 public class MeteostickSensorHandler extends BaseThingHandler implements MeteostickEventListener {
53 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DAVIS);
55 private final Logger logger = LoggerFactory.getLogger(MeteostickSensorHandler.class);
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;
66 private Date lastData;
68 public MeteostickSensorHandler(Thing thing) {
73 public void initialize() {
74 logger.debug("Initializing MeteoStick handler.");
76 channel = ((BigDecimal) getConfig().get(PARAMETER_CHANNEL)).intValue();
78 spoon = (BigDecimal) getConfig().get(PARAMETER_SPOON);
80 spoon = new BigDecimal(PARAMETER_SPOON_DEFAULT);
82 logger.debug("Initializing MeteoStick handler - Channel {}, Spoon size {} mm.", channel, spoon);
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)));
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);
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));
105 // Scheduling a job to run every two minutes to update wind statistics
106 wind2MinJob = scheduler.scheduleWithFixedDelay(windRunnable, 2, 2, TimeUnit.MINUTES);
108 updateStatus(ThingStatus.UNKNOWN);
112 public void dispose() {
113 if (rainHourlyJob != null) {
114 rainHourlyJob.cancel(true);
117 if (wind2MinJob != null) {
118 wind2MinJob.cancel(true);
121 if (offlineTimerJob != null) {
122 offlineTimerJob.cancel(true);
125 if (bridgeHandler != null) {
126 bridgeHandler.unsubscribeEvents(channel, this);
131 public void handleCommand(ChannelUID channelUID, Command command) {
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);
143 bridgeHandler = (MeteostickBridgeHandler) getBridge().getHandler();
146 if (bridgeHandler != null) {
147 bridgeHandler.subscribeEvents(channel, this);
151 // Put the thing online and start our "no data" timer
152 updateStatus(ThingStatus.ONLINE);
156 private void processSignalStrength(String dbmString) {
157 double dbm = Double.parseDouble(dbmString);
162 } else if (dbm > -70) {
164 } else if (dbm > -80) {
166 } else if (dbm > -90) {
172 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SIGNAL_STRENGTH), new DecimalType(strength));
175 private void processBattery(boolean batteryLow) {
176 OnOffType state = batteryLow ? OnOffType.ON : OnOffType.OFF;
178 updateState(new ChannelUID(getThing().getUID(), CHANNEL_LOW_BATTERY), state);
182 public void onDataReceived(String[] data) {
183 logger.debug("MeteoStick received channel {}: {}", channel, data);
184 updateStatus(ThingStatus.ONLINE);
185 lastData = new Date();
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);
196 rainHistory.put(rain);
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)));
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));
211 windHistory.put(windSpeed, windDirection);
213 processSignalStrength(data[4]);
214 processBattery(data.length == 6);
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));
221 BigDecimal humidity = new BigDecimal(data[3]);
222 updateState(new ChannelUID(getThing().getUID(), CHANNEL_HUMIDITY),
223 new DecimalType(humidity.setScale(1)));
225 processSignalStrength(data[4]);
226 processBattery(data.length == 6);
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)));
233 processSignalStrength(data[3]);
234 processBattery(data.length == 5);
239 class SlidingTimeWindow<T> {
240 private long period = 0;
241 protected final SortedMap<Long, T> storage = Collections.synchronizedSortedMap(new TreeMap<>());
245 * @param period window period in milliseconds
247 public SlidingTimeWindow(long period) {
248 this.period = period;
251 public void put(T value) {
252 storage.put(System.currentTimeMillis(), value);
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();
268 class RainHistory extends SlidingTimeWindow<Integer> {
270 public RainHistory(long period) {
274 public BigDecimal getTotal(BigDecimal spoon) {
280 synchronized (storage) {
281 for (int value : storage.values()) {
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.
297 total = 128 - least + value;
299 total = value - least;
304 return BigDecimal.valueOf(total).multiply(spoon);
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.
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);
327 BigDecimal averageSpeed;
328 int averageDirection;
332 class WindHistory extends SlidingTimeWindow<WindSample> {
334 public WindHistory(long period) {
338 public void put(BigDecimal speed, int directionDegrees) {
339 put(new WindSample(speed, directionDegrees));
342 public WindStats getStats() {
347 double totalSpeed = 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;
362 WindStats stats = new WindStats();
364 stats.averageDirection = (int) Math.toDegrees(Math.atan2(ewSum, nsSum));
365 if (stats.averageDirection < 0) {
366 stats.averageDirection += 360;
369 stats.averageSpeed = new BigDecimal(size > 0 ? totalSpeed / size : 0).setScale(3, RoundingMode.HALF_DOWN);
371 stats.maxSpeed = new BigDecimal(maxSpeed).setScale(3, RoundingMode.HALF_DOWN);
377 private synchronized void startTimeoutCheck() {
378 Runnable pollingRunnable = () -> {
380 if (lastData == null) {
381 detail = "No data received";
383 detail = "No data received since " + lastData.toString();
385 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
388 if (offlineTimerJob != null) {
389 offlineTimerJob.cancel(true);
392 // Scheduling a job on each hour to update the last hour rainfall
393 offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);