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 cloudiness) {
118 scheduler.submit(() -> {
119 sagerWeatherCaster.setCloudLevel(cloudiness.intValue());
124 case CHANNEL_IS_RAINING:
125 logger.debug("Rain status updated, updating forecast");
126 if (command instanceof OnOffType isRaining) {
127 scheduler.submit(() -> {
128 sagerWeatherCaster.setRaining(isRaining == OnOffType.ON);
132 logger.debug("Channel '{}' accepts Switch commands.", channelUID);
135 case CHANNEL_RAIN_QTTY:
136 logger.debug("Rain status updated, updating forecast");
137 if (command instanceof QuantityType quantityCommand) {
138 updateRain(quantityCommand);
139 } else if (command instanceof DecimalType decimalCommand) {
140 updateRain(decimalCommand);
142 logger.debug("Channel '{}' accepts Number, Number:(Speed|Length) commands.", channelUID);
145 case CHANNEL_WIND_SPEED:
146 logger.debug("Updated wind speed, updating forecast");
147 if (command instanceof DecimalType newValue) {
148 scheduler.submit(() -> {
149 sagerWeatherCaster.setBeaufort(newValue.intValue());
153 logger.debug("Channel '{}' only accepts DecimalType commands.", channelUID);
156 case CHANNEL_PRESSURE:
157 logger.debug("Sea-level pressure updated, updating forecast");
158 if (command instanceof QuantityType) {
159 @SuppressWarnings("unchecked")
160 QuantityType<Pressure> pressQtty = ((QuantityType<Pressure>) command)
161 .toUnit(HECTO(SIUnits.PASCAL));
162 if (pressQtty != null) {
163 double newPressureValue = pressQtty.doubleValue();
164 pressureCache.put(newPressureValue);
165 pressureCache.getAgedValue().ifPresentOrElse(oldPressure -> scheduler.submit(() -> {
166 sagerWeatherCaster.setPressure(newPressureValue, oldPressure);
167 updateChannelString(GROUP_OUTPUT, CHANNEL_PRESSURETREND,
168 String.valueOf(sagerWeatherCaster.getPressureEvolution()));
170 }), () -> updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, FORECAST_PENDING));
174 case CHANNEL_TEMPERATURE:
175 logger.debug("Temperature updated");
176 if (command instanceof QuantityType) {
177 @SuppressWarnings("unchecked")
178 QuantityType<Temperature> tempQtty = ((QuantityType<Temperature>) command)
179 .toUnit(SIUnits.CELSIUS);
180 if (tempQtty != null) {
181 currentTemp = tempQtty.doubleValue();
182 temperatureCache.put(currentTemp);
183 temperatureCache.getAgedValue().ifPresent(oldTemperature -> {
184 double delta = currentTemp - oldTemperature;
185 String trend = (delta > 3) ? "1"
186 : (delta > 0.3) ? "2" : (delta > -0.3) ? "3" : (delta > -3) ? "4" : "5";
187 updateChannelString(GROUP_OUTPUT, CHANNEL_TEMPERATURETREND, trend);
192 case CHANNEL_WIND_ANGLE:
193 logger.debug("Updated wind direction, updating forecast");
194 if (command instanceof QuantityType) {
195 @SuppressWarnings("unchecked")
196 QuantityType<Angle> angleQtty = (QuantityType<Angle>) command;
197 int newAngleValue = angleQtty.intValue();
198 bearingCache.put(newAngleValue);
199 bearingCache.getAgedValue().ifPresent(oldAngle -> {
200 scheduler.submit(() -> {
201 sagerWeatherCaster.setBearing(newAngleValue, oldAngle);
202 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
203 String.valueOf(sagerWeatherCaster.getWindEvolution()));
210 logger.debug("The binding can not handle command: {} on channel: {}", command, channelUID);
215 private void updateRain(Number newQtty) {
216 scheduler.submit(() -> {
217 sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
222 private void postNewForecast() {
223 String newSagerCode = sagerWeatherCaster.getSagerCode();
224 if (!newSagerCode.equals(currentSagerCode)) {
225 logger.debug("Sager prediction changed to {}", newSagerCode);
226 currentSagerCode = newSagerCode;
227 updateChannelTimeStamp(GROUP_OUTPUT, CHANNEL_TIMESTAMP, ZonedDateTime.now());
228 String forecast = sagerWeatherCaster.getForecast();
229 // Sharpens forecast if current temp is below 2 degrees, likely to be flurries rather than shower
230 forecast += SHOWERS.contains(forecast) ? (currentTemp > 2) ? "1" : "2" : "";
232 updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, forecast);
233 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDFROM, sagerWeatherCaster.getWindDirection());
234 updateChannelString(GROUP_OUTPUT, CHANNEL_WINDTO, sagerWeatherCaster.getWindDirection2());
235 updateChannelString(GROUP_OUTPUT, CHANNEL_VELOCITY, sagerWeatherCaster.getWindVelocity());
236 updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, sagerWeatherCaster.getPredictedBeaufort());
240 private void updateChannelTimeStamp(String group, String channelId, ZonedDateTime zonedDateTime) {
241 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
243 updateState(id, new DateTimeType(zonedDateTime));
247 private void updateChannelString(String group, String channelId, String value) {
248 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
250 updateState(id, new StringType(value));
254 private void updateChannelDecimal(String group, String channelId, int value) {
255 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
257 updateState(id, new DecimalType(value));