]> git.basschouten.com Git - openhab-addons.git/blob
f9555546589e5170b477ee581221c4d7238abb45
[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.airquality.internal.handler;
14
15 import static org.openhab.binding.airquality.internal.AirQualityBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.*;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Optional;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Stream;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.airquality.internal.AirQualityException;
32 import org.openhab.binding.airquality.internal.api.ApiBridge;
33 import org.openhab.binding.airquality.internal.api.Appreciation;
34 import org.openhab.binding.airquality.internal.api.Index;
35 import org.openhab.binding.airquality.internal.api.Pollutant;
36 import org.openhab.binding.airquality.internal.api.Pollutant.SensitiveGroup;
37 import org.openhab.binding.airquality.internal.api.dto.AirQualityData;
38 import org.openhab.binding.airquality.internal.config.AirQualityConfiguration;
39 import org.openhab.binding.airquality.internal.config.SensitiveGroupConfiguration;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.i18n.LocationProvider;
42 import org.openhab.core.i18n.TimeZoneProvider;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PointType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.types.RawType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.SIUnits;
51 import org.openhab.core.library.unit.Units;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.Channel;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.thing.binding.BridgeHandler;
60 import org.openhab.core.thing.binding.builder.ThingBuilder;
61 import org.openhab.core.thing.type.ChannelTypeUID;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * The {@link AirQualityStationHandler} is responsible for handling commands, which are
71  * sent to one of the channels.
72  *
73  * @author Kuba Wolanin - Initial contribution
74  * @author Ćukasz Dywicki - Initial contribution
75  */
76 @NonNullByDefault
77 public class AirQualityStationHandler extends BaseThingHandler {
78     private final @NonNullByDefault({}) ClassLoader classLoader = AirQualityStationHandler.class.getClassLoader();
79     private final Logger logger = LoggerFactory.getLogger(AirQualityStationHandler.class);
80     private final TimeZoneProvider timeZoneProvider;
81     private final LocationProvider locationProvider;
82
83     private @Nullable ScheduledFuture<?> refreshJob;
84
85     public AirQualityStationHandler(Thing thing, TimeZoneProvider timeZoneProvider, LocationProvider locationProvider) {
86         super(thing);
87         this.timeZoneProvider = timeZoneProvider;
88         this.locationProvider = locationProvider;
89     }
90
91     @Override
92     public void initialize() {
93         logger.debug("Initializing Air Quality handler.");
94
95         if (thing.getProperties().isEmpty()) {
96             discoverAttributes();
97         }
98
99         AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
100         try {
101             config.checkValid();
102             freeRefreshJob();
103             refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublishData, 0, config.refresh,
104                     TimeUnit.MINUTES);
105         } catch (AirQualityException e) {
106             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
107         }
108     }
109
110     private void discoverAttributes() {
111         getAirQualityData().ifPresent(data -> {
112             // Update thing properties
113             Map<String, String> properties = new HashMap<>();
114             properties.put(ATTRIBUTIONS, data.getAttributions());
115             PointType serverLocation = locationProvider.getLocation();
116             if (serverLocation != null) {
117                 PointType stationLocation = new PointType(data.getCity().getGeo());
118                 double distance = serverLocation.distanceFrom(stationLocation).doubleValue();
119                 properties.put(DISTANCE, new QuantityType<>(distance / 1000, KILO(SIUnits.METRE)).toString());
120             }
121
122             // Search and remove missing pollutant channels
123             List<Channel> channels = new ArrayList<>(getThing().getChannels());
124             Stream.of(Pollutant.values()).forEach(pollutant -> {
125                 String groupName = pollutant.name().toLowerCase();
126                 double value = data.getIaqiValue(pollutant);
127                 channels.removeIf(channel -> value == -1 && groupName.equals(channel.getUID().getGroupId()));
128             });
129
130             // Update thing configuration
131             Configuration config = editConfiguration();
132             config.put(AirQualityConfiguration.STATION_ID, data.getStationId());
133
134             ThingBuilder thingBuilder = editThing();
135             thingBuilder.withChannels(channels).withConfiguration(config).withProperties(properties)
136                     .withLocation(data.getCity().getName());
137             updateThing(thingBuilder.build());
138         });
139     }
140
141     private void updateAndPublishData() {
142         getAirQualityData().ifPresent(data -> {
143             getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID().getId())).forEach(channel -> {
144                 State state;
145                 ChannelUID channelUID = channel.getUID();
146                 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
147                 if (channelTypeUID != null && SENSITIVE.equals(channelTypeUID.getId().toString())) {
148                     SensitiveGroupConfiguration configuration = channel.getConfiguration()
149                             .as(SensitiveGroupConfiguration.class);
150                     state = getSensitive(configuration.asSensitiveGroup(), data);
151                 } else {
152                     state = getValue(channelUID.getIdWithoutGroup(), channelUID.getGroupId(), data);
153                 }
154                 updateState(channelUID, state);
155             });
156         });
157     }
158
159     @Override
160     public void dispose() {
161         logger.debug("Disposing the Air Quality handler.");
162         freeRefreshJob();
163     }
164
165     private void freeRefreshJob() {
166         ScheduledFuture<?> job = this.refreshJob;
167         if (job != null && !job.isCancelled()) {
168             job.cancel(true);
169             refreshJob = null;
170         }
171     }
172
173     @Override
174     public void handleCommand(ChannelUID channelUID, Command command) {
175         if (command instanceof RefreshType) {
176             updateAndPublishData();
177             return;
178         }
179         logger.debug("The Air Quality binding is read-only and can not handle command {}", command);
180     }
181
182     /**
183      * Request new air quality data to the aqicn.org service
184      *
185      * @return an optional air quality data object mapping the JSON response
186      */
187     private Optional<AirQualityData> getAirQualityData() {
188         AirQualityData result = null;
189         ApiBridge apiBridge = getApiBridge();
190         if (apiBridge != null) {
191             AirQualityConfiguration config = getConfigAs(AirQualityConfiguration.class);
192             try {
193                 result = apiBridge.getData(config.stationId, config.location, 0);
194                 updateStatus(ThingStatus.ONLINE);
195             } catch (AirQualityException e) {
196                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
197             }
198         }
199         return Optional.ofNullable(result);
200     }
201
202     private @Nullable ApiBridge getApiBridge() {
203         Bridge bridge = this.getBridge();
204         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
205             BridgeHandler handler = bridge.getHandler();
206             if (handler instanceof AirQualityBridgeHandler) {
207                 return ((AirQualityBridgeHandler) handler).getApiBridge();
208             } else {
209                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/incorrect-bridge");
210             }
211         } else {
212             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
213         }
214         return null;
215     }
216
217     private State indexedValue(String channelId, double idx, @Nullable Pollutant pollutant) {
218         Index index = Index.find(idx);
219         if (index != null) {
220             switch (channelId) {
221                 case INDEX:
222                     return new DecimalType(idx);
223                 case VALUE:
224                     return pollutant != null ? pollutant.toQuantity(idx) : UnDefType.UNDEF;
225                 case ICON:
226                     byte[] bytes = getResource(String.format("picto/%s.svg", index.getCategory().name().toLowerCase()));
227                     return bytes != null ? new RawType(bytes, "image/svg+xml") : UnDefType.UNDEF;
228                 case COLOR:
229                     return index.getCategory().getColor();
230                 case ALERT_LEVEL:
231                     return new DecimalType(index.getCategory().ordinal());
232             }
233         }
234         return UnDefType.UNDEF;
235     }
236
237     private State getSensitive(@Nullable SensitiveGroup sensitiveGroup, AirQualityData data) {
238         if (sensitiveGroup != null) {
239             int threshHold = Appreciation.UNHEALTHY_FSG.ordinal();
240             for (Pollutant pollutant : Pollutant.values()) {
241                 Index index = Index.find(data.getIaqiValue(pollutant));
242                 if (index != null && pollutant.sensitiveGroups.contains(sensitiveGroup)
243                         && index.getCategory().ordinal() >= threshHold) {
244                     return OnOffType.ON;
245                 }
246             }
247             return OnOffType.OFF;
248         }
249         return UnDefType.NULL;
250     }
251
252     private State getValue(String channelId, @Nullable String groupId, AirQualityData data) {
253         switch (channelId) {
254             case TEMPERATURE:
255                 double temp = data.getIaqiValue("t");
256                 return temp != -1 ? new QuantityType<>(temp, SIUnits.CELSIUS) : UnDefType.UNDEF;
257             case PRESSURE:
258                 double press = data.getIaqiValue("p");
259                 return press != -1 ? new QuantityType<>(press, HECTO(SIUnits.PASCAL)) : UnDefType.UNDEF;
260             case HUMIDITY:
261                 double hum = data.getIaqiValue("h");
262                 return hum != -1 ? new QuantityType<>(hum, Units.PERCENT) : UnDefType.UNDEF;
263             case TIMESTAMP:
264                 return new DateTimeType(
265                         data.getTime().getObservationTime().withZoneSameLocal(timeZoneProvider.getTimeZone()));
266             case DOMINENT:
267                 return new StringType(data.getDominentPol());
268             case DEW_POINT:
269                 double dp = data.getIaqiValue("dew");
270                 return dp != -1 ? new QuantityType<>(dp, SIUnits.CELSIUS) : UnDefType.UNDEF;
271             case WIND_SPEED:
272                 double w = data.getIaqiValue("w");
273                 return w != -1 ? new QuantityType<>(w, Units.METRE_PER_SECOND) : UnDefType.UNDEF;
274             default:
275                 if (groupId != null) {
276                     double idx = -1;
277                     Pollutant pollutant = null;
278                     if (AQI.equals(groupId)) {
279                         idx = data.getAqi();
280                     } else {
281                         pollutant = Pollutant.valueOf(groupId.toUpperCase());
282                         idx = data.getIaqiValue(pollutant);
283                     }
284                     return indexedValue(channelId, idx, pollutant);
285                 }
286                 return UnDefType.UNDEF;
287         }
288     }
289
290     private byte @Nullable [] getResource(String iconPath) {
291         try (InputStream stream = classLoader.getResourceAsStream(iconPath)) {
292             return stream != null ? stream.readAllBytes() : null;
293         } catch (IOException e) {
294             logger.warn("Unable to load ressource '{}' : {}", iconPath, e.getMessage());
295         }
296         return null;
297     }
298 }