]> git.basschouten.com Git - openhab-addons.git/blob
1f23a4fa7eb2f9080e631178fef3f67e8d19a7e8
[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.fmiweather.internal;
14
15 import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
16 import static org.openhab.binding.fmiweather.internal.client.ObservationRequest.*;
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.time.Instant;
22 import java.util.AbstractMap;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Objects;
27 import java.util.concurrent.TimeUnit;
28 import java.util.function.Function;
29
30 import javax.measure.Unit;
31 import javax.measure.quantity.Length;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.fmiweather.internal.client.Data;
36 import org.openhab.binding.fmiweather.internal.client.FMIResponse;
37 import org.openhab.binding.fmiweather.internal.client.FMISID;
38 import org.openhab.binding.fmiweather.internal.client.Location;
39 import org.openhab.binding.fmiweather.internal.client.ObservationRequest;
40 import org.openhab.binding.fmiweather.internal.client.Request;
41 import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
42 import org.openhab.core.library.unit.MetricPrefix;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link ObservationWeatherHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Sami Salonen - Initial contribution
57  */
58 @NonNullByDefault
59 public class ObservationWeatherHandler extends AbstractWeatherHandler {
60
61     private final Logger logger = LoggerFactory.getLogger(ObservationWeatherHandler.class);
62     private static final long OBSERVATION_LOOK_BACK_SECONDS = TimeUnit.MINUTES.toSeconds(30);
63     private static final int STEP_MINUTES = 10;
64     private static final int POLL_INTERVAL_SECONDS = 600;
65     private static BigDecimal HUNDRED = BigDecimal.valueOf(100);
66     private static BigDecimal NA_CLOUD_MAX = BigDecimal.valueOf(8); // API value when having full clouds (overcast)
67     private static BigDecimal NA_CLOUD_COVERAGE = BigDecimal.valueOf(9); // API value when cloud coverage could not be
68                                                                          // determined.
69
70     public static final Unit<Length> MILLIMETRE = MetricPrefix.MILLI(METRE);
71     public static final Unit<Length> CENTIMETRE = MetricPrefix.CENTI(METRE);
72
73     private static final Map<String, Map.Entry<String, @Nullable Unit<?>>> CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT = new HashMap<>(
74             11);
75     private static final Map<String, @Nullable Function<BigDecimal, @Nullable BigDecimal>> OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC = new HashMap<>(
76             11);
77
78     private static void addMapping(String channelId, String requestField, @Nullable Unit<?> result_unit,
79             @Nullable Function<BigDecimal, @Nullable BigDecimal> conversion) {
80         CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT.put(channelId,
81                 new AbstractMap.SimpleImmutableEntry<>(requestField, result_unit));
82         OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC.put(requestField, conversion);
83     }
84
85     static {
86         addMapping(CHANNEL_TEMPERATURE, PARAM_TEMPERATURE, CELSIUS, null);
87         addMapping(CHANNEL_HUMIDITY, PARAM_HUMIDITY, PERCENT, null);
88         addMapping(CHANNEL_WIND_DIRECTION, PARAM_WIND_DIRECTION, DEGREE_ANGLE, null);
89         addMapping(CHANNEL_WIND_SPEED, PARAM_WIND_SPEED, METRE_PER_SECOND, null);
90         addMapping(CHANNEL_GUST, PARAM_WIND_GUST, METRE_PER_SECOND, null);
91         addMapping(CHANNEL_PRESSURE, PARAM_PRESSURE, MILLIBAR, null);
92         addMapping(CHANNEL_PRECIPITATION_AMOUNT, PARAM_PRECIPITATION_AMOUNT, MILLIMETRE, null);
93         addMapping(CHANNEL_SNOW_DEPTH, PARAM_SNOW_DEPTH, CENTIMETRE, null);
94         addMapping(CHANNEL_VISIBILITY, PARAM_VISIBILITY, METRE, null);
95         // Converting 0...8 scale to percentage 0...100%. Value of 9 is converted to null/UNDEF
96         addMapping(CHANNEL_CLOUDS, PARAM_CLOUDS, PERCENT, clouds -> clouds.compareTo(NA_CLOUD_COVERAGE) == 0 ? null
97                 : clouds.divide(NA_CLOUD_MAX).multiply(HUNDRED));
98         addMapping(CHANNEL_OBSERVATION_PRESENT_WEATHER, PARAM_PRESENT_WEATHER, null, null);
99     }
100
101     private @NonNullByDefault({}) String fmisid;
102
103     public ObservationWeatherHandler(Thing thing) {
104         super(thing);
105         pollIntervalSeconds = POLL_INTERVAL_SECONDS;
106     }
107
108     @Override
109     public void initialize() {
110         fmisid = Objects.toString(getConfig().get(BindingConstants.FMISID), null);
111         if (fmisid == null) {
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
113                     String.format("%s parameter not set", FMISID));
114         } else {
115             super.initialize();
116         }
117     }
118
119     @Override
120     protected Request getRequest() {
121         long now = Instant.now().getEpochSecond();
122         return new ObservationRequest(new FMISID(fmisid),
123                 floorToEvenMinutes(now - OBSERVATION_LOOK_BACK_SECONDS, STEP_MINUTES),
124                 ceilToEvenMinutes(now, STEP_MINUTES), STEP_MINUTES);
125     }
126
127     @Override
128     protected void updateChannels() {
129         FMIResponse response = this.response;
130         if (response == null) {
131             return;
132         }
133         try {
134             Location location = unwrap(response.getLocations().stream().findFirst(),
135                     "No locations in response -- no data? Aborting");
136             Map<String, String> properties = editProperties();
137             properties.put(PROP_NAME, location.name);
138             properties.put(PROP_LATITUDE, location.latitude.toPlainString());
139             properties.put(PROP_LONGITUDE, location.longitude.toPlainString());
140             updateProperties(properties);
141             // All parameters and locations should share the same timestamps. We use temperature to figure out most
142             // recent timestamp which has non-NaN value
143             int lastValidIndex = unwrap(
144                     response.getData(location, ObservationRequest.PARAM_TEMPERATURE).map(data -> lastValidIndex(data)),
145                     "lastValidIndex not available. Bug?");
146             for (Channel channel : getThing().getChannels()) {
147                 ChannelUID channelUID = channel.getUID();
148                 if (lastValidIndex < 0) {
149                     updateState(channelUID, UnDefType.UNDEF);
150                 } else if (channelUID.getIdWithoutGroup().equals(CHANNEL_TIME)) {
151                     String field = ObservationRequest.PARAM_TEMPERATURE;
152                     Data data = unwrap(response.getData(location, field),
153                             "Field %s not present for location %s in response. Bug?", field, location);
154                     updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[lastValidIndex]);
155                 } else {
156                     String field = getDataField(channelUID);
157                     Unit<?> unit = getUnit(channelUID);
158                     if (field == null) {
159                         logger.error("Channel {} not handled. Bug?", channelUID.getId());
160                         continue;
161                     }
162                     Data data = unwrap(response.getData(location, field),
163                             "Field %s not present for location % in response. Bug?", field, location);
164                     BigDecimal rawValue = data.values[lastValidIndex];
165                     BigDecimal processedValue = preprocess(field, rawValue);
166                     updateStateIfLinked(channelUID, processedValue, unit);
167                 }
168             }
169             updateStatus(ThingStatus.ONLINE);
170         } catch (FMIUnexpectedResponseException e) {
171             // Unexpected (possibly bug) issue with response
172             logger.warn("Unexpected response encountered: {}", e.getMessage());
173             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
174                     String.format("Unexpected API response: %s", e.getMessage()));
175         }
176     }
177
178     @SuppressWarnings({ "null", "unused" })
179     private static @Nullable String getDataField(ChannelUID channelUID) {
180         Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT
181                 .get(channelUID.getIdWithoutGroup());
182         if (entry == null) {
183             return null;
184         }
185         return entry.getKey();
186     }
187
188     @SuppressWarnings({ "null", "unused" })
189     private static @Nullable Unit<?> getUnit(ChannelUID channelUID) {
190         Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT
191                 .get(channelUID.getIdWithoutGroup());
192         if (entry == null) {
193             return null;
194         }
195         return entry.getValue();
196     }
197
198     private static @Nullable BigDecimal preprocess(String fieldName, @Nullable BigDecimal rawValue) {
199         if (rawValue == null) {
200             return null;
201         }
202         Function<BigDecimal, @Nullable BigDecimal> func = OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC.get(fieldName);
203         if (func == null) {
204             // No conversion required
205             return rawValue;
206         }
207         return func.apply(rawValue);
208     }
209 }