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.sagercaster.internal.handler;
15 import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.HECTO;
18 import java.math.BigDecimal;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.concurrent.TimeUnit;
24 import javax.measure.quantity.Angle;
25 import javax.measure.quantity.Pressure;
26 import javax.measure.quantity.Temperature;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.openhab.binding.sagercaster.internal.WindDirectionStateDescriptionProvider;
30 import org.openhab.binding.sagercaster.internal.caster.SagerWeatherCaster;
31 import org.openhab.core.library.types.DateTimeType;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingUID;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.StateOption;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link SagerCasterHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Gaƫl L'hopital - Initial contribution
55 public class SagerCasterHandler extends BaseThingHandler {
57 private final Logger logger = LoggerFactory.getLogger(SagerCasterHandler.class);
59 private final SagerWeatherCaster sagerWeatherCaster;
61 private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
63 private final ExpiringMap<Double> pressureCache = new ExpiringMap<>();
64 private final ExpiringMap<Double> temperatureCache = new ExpiringMap<>();
65 private final ExpiringMap<Integer> bearingCache = new ExpiringMap<>();
67 private double currentTemp = 0;
68 private String currentSagerCode = SagerWeatherCaster.UNDEF;
70 public SagerCasterHandler(Thing thing, WindDirectionStateDescriptionProvider stateDescriptionProvider,
71 SagerWeatherCaster sagerWeatherCaster) {
73 this.stateDescriptionProvider = stateDescriptionProvider;
74 this.sagerWeatherCaster = sagerWeatherCaster;
78 public void initialize() {
79 int observationPeriod = ((BigDecimal) getConfig().get(CONFIG_PERIOD)).intValue();
80 long period = TimeUnit.HOURS.toMillis(observationPeriod);
81 pressureCache.setObservationPeriod(period);
82 bearingCache.setObservationPeriod(period);
83 temperatureCache.setObservationPeriod(period);
85 String location = (String) getConfig().get(CONFIG_LOCATION);
86 String latitude = location.split(",")[0];
87 sagerWeatherCaster.setLatitude(Double.parseDouble(latitude));
88 defineWindDirectionStateDescriptions();
90 updateStatus(ThingStatus.ONLINE);
93 private void defineWindDirectionStateDescriptions() {
94 List<StateOption> options = new ArrayList<>();
95 String[] directions = sagerWeatherCaster.getUsedDirections();
96 for (int i = 0; i < directions.length; i++) {
97 int secondDirection = i < directions.length - 1 ? i + 1 : 0;
98 String windDescription = directions[i] + " or " + directions[secondDirection] + " winds";
99 options.add(new StateOption(String.valueOf(i + 1), windDescription));
102 options.add(new StateOption("9", "Shifting / Variable winds"));
103 ThingUID thingUID = getThing().getUID();
104 stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, GROUP_OUTPUT, CHANNEL_WINDFROM), options);
105 stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, GROUP_OUTPUT, CHANNEL_WINDTO), options);
109 public void handleCommand(ChannelUID channelUID, Command command) {
110 if (command instanceof RefreshType) {
113 String id = channelUID.getIdWithoutGroup();
115 case CHANNEL_CLOUDINESS:
116 logger.debug("Cloud level changed, updating forecast");
117 if (command instanceof QuantityType) {
118 QuantityType<?> cloudiness = (QuantityType<?>) command;
119 scheduler.submit(() -> {
120 sagerWeatherCaster.setCloudLevel(cloudiness.intValue());
125 case CHANNEL_IS_RAINING:
126 logger.debug("Rain status updated, updating forecast");
127 if (command instanceof OnOffType) {
128 OnOffType isRaining = (OnOffType) command;
129 scheduler.submit(() -> {
130 sagerWeatherCaster.setRaining(isRaining == OnOffType.ON);
134 logger.debug("Channel '{}' accepts Switch commands.", channelUID);
137 case CHANNEL_RAIN_QTTY:
138 logger.debug("Rain status updated, updating forecast");
139 if (command instanceof QuantityType) {
140 updateRain((QuantityType<?>) command);
141 } else if (command instanceof DecimalType) {
142 updateRain((DecimalType) command);
144 logger.debug("Channel '{}' accepts Number, Number:(Speed|Length) commands.", channelUID);
147 case CHANNEL_WIND_SPEED:
148 logger.debug("Updated wind speed, updating forecast");
149 if (command instanceof DecimalType) {
150 DecimalType newValue = (DecimalType) command;
151 scheduler.submit(() -> {
152 sagerWeatherCaster.setBeaufort(newValue.intValue());
156 logger.debug("Channel '{}' only accepts DecimalType commands.", channelUID);
159 case CHANNEL_PRESSURE:
160 logger.debug("Sea-level pressure updated, updating forecast");
161 if (command instanceof QuantityType) {
162 @SuppressWarnings("unchecked")
163 QuantityType<Pressure> pressQtty = ((QuantityType<Pressure>) command)
164 .toUnit(HECTO(SIUnits.PASCAL));
165 if (pressQtty != null) {
166 double newPressureValue = pressQtty.doubleValue();
167 pressureCache.put(newPressureValue);
168 pressureCache.getAgedValue().ifPresentOrElse(oldPressure -> scheduler.submit(() -> {
169 sagerWeatherCaster.setPressure(newPressureValue, oldPressure);
170 updateChannelString(GROUP_OUTPUT, CHANNEL_PRESSURETREND,
171 String.valueOf(sagerWeatherCaster.getPressureEvolution()));
173 }), () -> updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, FORECAST_PENDING));
177 case CHANNEL_TEMPERATURE:
178 logger.debug("Temperature updated");
179 if (command instanceof QuantityType) {
180 @SuppressWarnings("unchecked")
181 QuantityType<Temperature> tempQtty = ((QuantityType<Temperature>) command)
182 .toUnit(SIUnits.CELSIUS);
183 if (tempQtty != null) {
184 currentTemp = tempQtty.doubleValue();
185 temperatureCache.put(currentTemp);
186 temperatureCache.getAgedValue().ifPresent(oldTemperature -> {
187 double delta = currentTemp - oldTemperature;
188 String trend = (delta > 3) ? "1"
189 : (delta > 0.3) ? "2" : (delta > -0.3) ? "3" : (delta > -3) ? "4" : "5";
190 updateChannelString(GROUP_OUTPUT, CHANNEL_TEMPERATURETREND, trend);
195 case CHANNEL_WIND_ANGLE:
196 logger.debug("Updated wind direction, updating forecast");
197 if (command instanceof QuantityType) {
198 @SuppressWarnings("unchecked")
199 QuantityType<Angle> angleQtty = (QuantityType<Angle>) command;
200 int newAngleValue = angleQtty.intValue();
201 bearingCache.put(newAngleValue);
202 bearingCache.getAgedValue().ifPresent(oldAngle -> {
203 scheduler.submit(() -> {
204 sagerWeatherCaster.setBearing(newAngleValue, oldAngle);
205 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
206 String.valueOf(sagerWeatherCaster.getWindEvolution()));
213 logger.debug("The binding can not handle command: {} on channel: {}", command, channelUID);
218 private void updateRain(Number newQtty) {
219 scheduler.submit(() -> {
220 sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
225 private void postNewForecast() {
226 String newSagerCode = sagerWeatherCaster.getSagerCode();
227 if (!newSagerCode.equals(currentSagerCode)) {
228 logger.debug("Sager prediction changed to {}", newSagerCode);
229 currentSagerCode = newSagerCode;
230 updateChannelTimeStamp(GROUP_OUTPUT, CHANNEL_TIMESTAMP, ZonedDateTime.now());
231 String forecast = sagerWeatherCaster.getForecast();
232 // Sharpens forecast if current temp is below 2 degrees, likely to be flurries rather than shower
233 forecast += SHOWERS.contains(forecast) ? (currentTemp > 2) ? "1" : "2" : "";
235 updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, forecast);
236 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDFROM, sagerWeatherCaster.getWindDirection());
237 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDTO, sagerWeatherCaster.getWindDirection2());
238 updateChannelString(GROUP_OUTPUT, CHANNEL_VELOCITY, sagerWeatherCaster.getWindVelocity());
239 updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, sagerWeatherCaster.getPredictedBeaufort());
243 private void updateChannelTimeStamp(String group, String channelId, ZonedDateTime zonedDateTime) {
244 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
246 updateState(id, new DateTimeType(zonedDateTime));
250 private void updateChannelString(String group, String channelId, String value) {
251 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
253 updateState(id, new StringType(value));
257 private void updateChannelDecimal(String group, String channelId, int value) {
258 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
260 updateState(id, new DecimalType(value));