]> git.basschouten.com Git - openhab-addons.git/blob
f599cd59b42df510cd7e37cbec37982317d83ed4
[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.sensebox.internal.handler;
14
15 import static org.openhab.binding.sensebox.internal.SenseBoxBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.Map;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.sensebox.internal.SenseBoxAPIConnection;
25 import org.openhab.binding.sensebox.internal.config.SenseBoxConfiguration;
26 import org.openhab.binding.sensebox.internal.dto.SenseBoxData;
27 import org.openhab.binding.sensebox.internal.dto.SenseBoxLocation;
28 import org.openhab.binding.sensebox.internal.dto.SenseBoxSensor;
29 import org.openhab.core.cache.ExpiringCacheMap;
30 import org.openhab.core.library.types.DateTimeType;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.PointType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.unit.MetricPrefix;
35 import org.openhab.core.library.unit.SIUnits;
36 import org.openhab.core.library.unit.Units;
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.ThingStatusDetail;
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.State;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * The {@link SenseBoxHandler} is responsible for handling commands, which are
51  * sent to one of the channels.
52  *
53  * @author Hakan Tandogan - Initial contribution
54  * @author Hakan Tandogan - Ignore incorrect data for brightness readings
55  * @author Hakan Tandogan - Changed use of caching utils to ESH ExpiringCacheMap
56  * @author Hakan Tandogan - Unit of Measurement support
57  */
58 @NonNullByDefault
59 public class SenseBoxHandler extends BaseThingHandler {
60
61     private final Logger logger = LoggerFactory.getLogger(SenseBoxHandler.class);
62
63     protected @NonNullByDefault({}) SenseBoxConfiguration thingConfiguration;
64
65     private @Nullable SenseBoxData data;
66
67     private @Nullable ScheduledFuture<?> refreshJob;
68
69     private static final BigDecimal ONEHUNDRED = BigDecimal.valueOf(100l);
70
71     private static final String CACHE_KEY_DATA = "DATA";
72
73     private final ExpiringCacheMap<String, SenseBoxData> cache = new ExpiringCacheMap<>(CACHE_EXPIRY);
74
75     private final SenseBoxAPIConnection connection = new SenseBoxAPIConnection();
76
77     public SenseBoxHandler(Thing thing) {
78         super(thing);
79     }
80
81     @Override
82     public void initialize() {
83         logger.debug("Start initializing!");
84
85         thingConfiguration = getConfigAs(SenseBoxConfiguration.class);
86
87         String senseBoxId = thingConfiguration.getSenseBoxId();
88         logger.debug("Thing Configuration {} initialized {}", getThing().getUID(), senseBoxId);
89
90         String offlineReason = "";
91         boolean validConfig = true;
92
93         if (senseBoxId == null || senseBoxId.trim().isEmpty()) {
94             offlineReason = "senseBox ID is mandatory and must be configured";
95             validConfig = false;
96         }
97
98         if (thingConfiguration.getRefreshInterval() < MINIMUM_UPDATE_INTERVAL) {
99             logger.warn("Refresh interval is much too small, setting to default of {} seconds",
100                     MINIMUM_UPDATE_INTERVAL);
101             thingConfiguration.setRefreshInterval(MINIMUM_UPDATE_INTERVAL);
102         }
103
104         if (senseBoxId != null && validConfig) {
105             cache.put(CACHE_KEY_DATA, () -> {
106                 return connection.reallyFetchDataFromServer(senseBoxId);
107             });
108             updateStatus(ThingStatus.UNKNOWN);
109             startAutomaticRefresh();
110         } else {
111             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, offlineReason);
112         }
113         logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
114     }
115
116     @Override
117     public void handleCommand(ChannelUID channelUID, Command command) {
118         if (command instanceof RefreshType) {
119             data = fetchData();
120             if (data != null && ThingStatus.ONLINE == data.getStatus()) {
121                 publishDataForChannel(channelUID);
122
123                 updateStatus(ThingStatus.ONLINE);
124             } else {
125                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
126             }
127         } else {
128             logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
129         }
130     }
131
132     @Override
133     public void dispose() {
134         stopAutomaticRefresh();
135     }
136
137     private void stopAutomaticRefresh() {
138         if (refreshJob != null) {
139             refreshJob.cancel(true);
140         }
141     }
142
143     private void publishData() {
144         logger.debug("Refreshing data for box {}, scheduled after {} seconds...", thingConfiguration.getSenseBoxId(),
145                 thingConfiguration.getRefreshInterval());
146         data = fetchData();
147         if (data != null && ThingStatus.ONLINE == data.getStatus()) {
148             publishProperties();
149             publishChannels();
150             updateStatus(ThingStatus.ONLINE);
151         } else {
152             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
153         }
154     };
155
156     private void startAutomaticRefresh() {
157         stopAutomaticRefresh();
158         refreshJob = scheduler.scheduleWithFixedDelay(this::publishData, 0, thingConfiguration.getRefreshInterval(),
159                 TimeUnit.SECONDS);
160     }
161
162     private @Nullable SenseBoxData fetchData() {
163         return cache.get(CACHE_KEY_DATA);
164     }
165
166     private void publishProperties() {
167         SenseBoxData localData = data;
168         if (localData != null) {
169             Map<String, String> properties = editProperties();
170             properties.put(PROPERTY_NAME, localData.getName());
171             properties.put(PROPERTY_EXPOSURE, localData.getExposure());
172             properties.put(PROPERTY_IMAGE_URL, localData.getDescriptor().getImageUrl());
173             properties.put(PROPERTY_MAP_URL, localData.getDescriptor().getMapUrl());
174             updateProperties(properties);
175         }
176     }
177
178     private void publishChannels() {
179         thing.getChannels().forEach(channel -> publishDataForChannel(channel.getUID()));
180     }
181
182     private void publishDataForChannel(ChannelUID channelUID) {
183         SenseBoxData localData = data;
184         if (localData != null && isLinked(channelUID)) {
185             switch (channelUID.getId()) {
186                 case CHANNEL_LOCATION:
187                     updateState(channelUID, locationFromData(localData.getLocation()));
188                     break;
189                 case CHANNEL_UV_INTENSITY:
190                     updateState(channelUID, decimalFromSensor(localData.getUvIntensity()));
191                     break;
192                 case CHANNEL_ILLUMINANCE:
193                     updateState(channelUID, decimalFromSensor(localData.getLuminance()));
194                     break;
195                 case CHANNEL_PRESSURE:
196                     updateState(channelUID, decimalFromSensor(localData.getPressure()));
197                     break;
198                 case CHANNEL_HUMIDITY:
199                     updateState(channelUID, decimalFromSensor(localData.getHumidity()));
200                     break;
201                 case CHANNEL_TEMPERATURE:
202                     updateState(channelUID, decimalFromSensor(localData.getTemperature()));
203                     break;
204                 case CHANNEL_PARTICULATE_MATTER_2_5:
205                     updateState(channelUID, decimalFromSensor(localData.getParticulateMatter2dot5()));
206                     break;
207                 case CHANNEL_PARTICULATE_MATTER_10:
208                     updateState(channelUID, decimalFromSensor(localData.getParticulateMatter10()));
209                     break;
210                 case CHANNEL_UV_INTENSITY_LR:
211                     updateState(channelUID, dateTimeFromSensor(localData.getUvIntensity()));
212                     break;
213                 case CHANNEL_ILLUMINANCE_LR:
214                     updateState(channelUID, dateTimeFromSensor(localData.getLuminance()));
215                     break;
216                 case CHANNEL_PRESSURE_LR:
217                     updateState(channelUID, dateTimeFromSensor(localData.getPressure()));
218                     break;
219                 case CHANNEL_HUMIDITY_LR:
220                     updateState(channelUID, dateTimeFromSensor(localData.getHumidity()));
221                     break;
222                 case CHANNEL_TEMPERATURE_LR:
223                     updateState(channelUID, dateTimeFromSensor(localData.getTemperature()));
224                     break;
225                 case CHANNEL_PARTICULATE_MATTER_2_5_LR:
226                     updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter2dot5()));
227                     break;
228                 case CHANNEL_PARTICULATE_MATTER_10_LR:
229                     updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter10()));
230                     break;
231                 default:
232                     logger.debug("Command received for an unknown channel: {}", channelUID.getId());
233                     break;
234             }
235         }
236     }
237
238     private State dateTimeFromSensor(@Nullable SenseBoxSensor sensorData) {
239         State result = UnDefType.UNDEF;
240         if (sensorData != null && sensorData.getLastMeasurement() != null
241                 && sensorData.getLastMeasurement().getCreatedAt() != null
242                 && !sensorData.getLastMeasurement().getCreatedAt().isEmpty()) {
243             result = new DateTimeType(sensorData.getLastMeasurement().getCreatedAt());
244         }
245         return result;
246     }
247
248     private State decimalFromSensor(@Nullable SenseBoxSensor sensorData) {
249         State result = UnDefType.UNDEF;
250         if (sensorData != null && sensorData.getLastMeasurement() != null
251                 && sensorData.getLastMeasurement().getValue() != null
252                 && !sensorData.getLastMeasurement().getValue().isEmpty()) {
253             logger.debug("About to determine quantity for {} / {}", sensorData.getLastMeasurement().getValue(),
254                     sensorData.getUnit());
255             BigDecimal bd = new BigDecimal(sensorData.getLastMeasurement().getValue());
256             switch (sensorData.getUnit()) {
257                 case "%":
258                     result = new QuantityType<>(bd, Units.PERCENT);
259                     break;
260                 case "°C":
261                     result = new QuantityType<>(bd, SIUnits.CELSIUS);
262                     break;
263                 case "Pa":
264                     result = new QuantityType<>(bd, SIUnits.PASCAL);
265                     break;
266                 case "hPa":
267                     if (BigDecimal.valueOf(10000l).compareTo(bd) < 0) {
268                         // Some stations report measurements in Pascal, but send 'hPa' as units...
269                         bd = bd.divide(ONEHUNDRED);
270                     }
271                     result = new QuantityType<>(bd, MetricPrefix.HECTO(SIUnits.PASCAL));
272                     break;
273                 case "lx":
274                     result = new QuantityType<>(bd, Units.LUX);
275                     break;
276                 case "\u00b5g/m³":
277                     result = new QuantityType<>(bd, Units.MICROGRAM_PER_CUBICMETRE);
278                     break;
279                 case "\u00b5W/cm²":
280                     result = new QuantityType<>(bd, Units.MICROWATT_PER_SQUARE_CENTIMETRE);
281                     break;
282                 default:
283                     // The data provider might have configured some unknown unit, accept at least the
284                     // measurement
285                     logger.debug("Could not determine unit for '{}', using default", sensorData.getUnit());
286                     result = new QuantityType<>(bd, Units.ONE);
287             }
288             logger.debug("State: '{}'", result);
289         }
290         return result;
291     }
292
293     private State locationFromData(@Nullable SenseBoxLocation locationData) {
294         State result = UnDefType.UNDEF;
295         if (locationData != null) {
296             result = new PointType(new DecimalType(locationData.getLatitude()),
297                     new DecimalType(locationData.getLongitude()), new DecimalType(locationData.getHeight()));
298         }
299         return result;
300     }
301 }