]> git.basschouten.com Git - openhab-addons.git/blob
e296061367d945482867aff20df104dfa5d3a985
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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, () -> connection.reallyFetchDataFromServer(senseBoxId));
106             updateStatus(ThingStatus.UNKNOWN);
107             startAutomaticRefresh();
108         } else {
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, offlineReason);
110         }
111         logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
112     }
113
114     @Override
115     public void handleCommand(ChannelUID channelUID, Command command) {
116         if (command instanceof RefreshType) {
117             data = fetchData();
118             if (data != null && ThingStatus.ONLINE == data.getStatus()) {
119                 publishDataForChannel(channelUID);
120
121                 updateStatus(ThingStatus.ONLINE);
122             } else {
123                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
124             }
125         } else {
126             logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
127         }
128     }
129
130     @Override
131     public void dispose() {
132         stopAutomaticRefresh();
133     }
134
135     private void stopAutomaticRefresh() {
136         if (refreshJob != null) {
137             refreshJob.cancel(true);
138         }
139     }
140
141     private void publishData() {
142         logger.debug("Refreshing data for box {}, scheduled after {} seconds...", thingConfiguration.getSenseBoxId(),
143                 thingConfiguration.getRefreshInterval());
144         data = fetchData();
145         if (data != null && ThingStatus.ONLINE == data.getStatus()) {
146             publishProperties();
147             publishChannels();
148             updateStatus(ThingStatus.ONLINE);
149         } else {
150             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
151         }
152     };
153
154     private void startAutomaticRefresh() {
155         stopAutomaticRefresh();
156         refreshJob = scheduler.scheduleWithFixedDelay(this::publishData, 0, thingConfiguration.getRefreshInterval(),
157                 TimeUnit.SECONDS);
158     }
159
160     private @Nullable SenseBoxData fetchData() {
161         return cache.get(CACHE_KEY_DATA);
162     }
163
164     private void publishProperties() {
165         SenseBoxData localData = data;
166         if (localData != null) {
167             Map<String, String> properties = editProperties();
168             properties.put(PROPERTY_NAME, localData.getName());
169             properties.put(PROPERTY_EXPOSURE, localData.getExposure());
170             properties.put(PROPERTY_IMAGE_URL, localData.getDescriptor().getImageUrl());
171             properties.put(PROPERTY_MAP_URL, localData.getDescriptor().getMapUrl());
172             updateProperties(properties);
173         }
174     }
175
176     private void publishChannels() {
177         thing.getChannels().forEach(channel -> publishDataForChannel(channel.getUID()));
178     }
179
180     private void publishDataForChannel(ChannelUID channelUID) {
181         SenseBoxData localData = data;
182         if (localData != null && isLinked(channelUID)) {
183             switch (channelUID.getId()) {
184                 case CHANNEL_LOCATION:
185                     updateState(channelUID, locationFromData(localData.getLocation()));
186                     break;
187                 case CHANNEL_UV_INTENSITY:
188                     updateState(channelUID, decimalFromSensor(localData.getUvIntensity()));
189                     break;
190                 case CHANNEL_ILLUMINANCE:
191                     updateState(channelUID, decimalFromSensor(localData.getLuminance()));
192                     break;
193                 case CHANNEL_PRESSURE:
194                     updateState(channelUID, decimalFromSensor(localData.getPressure()));
195                     break;
196                 case CHANNEL_HUMIDITY:
197                     updateState(channelUID, decimalFromSensor(localData.getHumidity()));
198                     break;
199                 case CHANNEL_TEMPERATURE:
200                     updateState(channelUID, decimalFromSensor(localData.getTemperature()));
201                     break;
202                 case CHANNEL_PARTICULATE_MATTER_2_5:
203                     updateState(channelUID, decimalFromSensor(localData.getParticulateMatter2dot5()));
204                     break;
205                 case CHANNEL_PARTICULATE_MATTER_10:
206                     updateState(channelUID, decimalFromSensor(localData.getParticulateMatter10()));
207                     break;
208                 case CHANNEL_UV_INTENSITY_LR:
209                     updateState(channelUID, dateTimeFromSensor(localData.getUvIntensity()));
210                     break;
211                 case CHANNEL_ILLUMINANCE_LR:
212                     updateState(channelUID, dateTimeFromSensor(localData.getLuminance()));
213                     break;
214                 case CHANNEL_PRESSURE_LR:
215                     updateState(channelUID, dateTimeFromSensor(localData.getPressure()));
216                     break;
217                 case CHANNEL_HUMIDITY_LR:
218                     updateState(channelUID, dateTimeFromSensor(localData.getHumidity()));
219                     break;
220                 case CHANNEL_TEMPERATURE_LR:
221                     updateState(channelUID, dateTimeFromSensor(localData.getTemperature()));
222                     break;
223                 case CHANNEL_PARTICULATE_MATTER_2_5_LR:
224                     updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter2dot5()));
225                     break;
226                 case CHANNEL_PARTICULATE_MATTER_10_LR:
227                     updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter10()));
228                     break;
229                 default:
230                     logger.debug("Command received for an unknown channel: {}", channelUID.getId());
231                     break;
232             }
233         }
234     }
235
236     private State dateTimeFromSensor(@Nullable SenseBoxSensor sensorData) {
237         State result = UnDefType.UNDEF;
238         if (sensorData != null && sensorData.getLastMeasurement() != null
239                 && sensorData.getLastMeasurement().getCreatedAt() != null
240                 && !sensorData.getLastMeasurement().getCreatedAt().isEmpty()) {
241             result = new DateTimeType(sensorData.getLastMeasurement().getCreatedAt());
242         }
243         return result;
244     }
245
246     private State decimalFromSensor(@Nullable SenseBoxSensor sensorData) {
247         State result = UnDefType.UNDEF;
248         if (sensorData != null && sensorData.getLastMeasurement() != null
249                 && sensorData.getLastMeasurement().getValue() != null
250                 && !sensorData.getLastMeasurement().getValue().isEmpty()) {
251             logger.debug("About to determine quantity for {} / {}", sensorData.getLastMeasurement().getValue(),
252                     sensorData.getUnit());
253             BigDecimal bd = new BigDecimal(sensorData.getLastMeasurement().getValue());
254             switch (sensorData.getUnit()) {
255                 case "%":
256                     result = new QuantityType<>(bd, Units.PERCENT);
257                     break;
258                 case "°C":
259                     result = new QuantityType<>(bd, SIUnits.CELSIUS);
260                     break;
261                 case "Pa":
262                     result = new QuantityType<>(bd, SIUnits.PASCAL);
263                     break;
264                 case "hPa":
265                     if (BigDecimal.valueOf(10000l).compareTo(bd) < 0) {
266                         // Some stations report measurements in Pascal, but send 'hPa' as units...
267                         bd = bd.divide(ONEHUNDRED);
268                     }
269                     result = new QuantityType<>(bd, MetricPrefix.HECTO(SIUnits.PASCAL));
270                     break;
271                 case "lx":
272                     result = new QuantityType<>(bd, Units.LUX);
273                     break;
274                 case "\u00b5g/m³":
275                     result = new QuantityType<>(bd, Units.MICROGRAM_PER_CUBICMETRE);
276                     break;
277                 case "\u00b5W/cm²":
278                     result = new QuantityType<>(bd, Units.MICROWATT_PER_SQUARE_CENTIMETRE);
279                     break;
280                 default:
281                     // The data provider might have configured some unknown unit, accept at least the
282                     // measurement
283                     logger.debug("Could not determine unit for '{}', using default", sensorData.getUnit());
284                     result = new QuantityType<>(bd, Units.ONE);
285             }
286             logger.debug("State: '{}'", result);
287         }
288         return result;
289     }
290
291     private State locationFromData(@Nullable SenseBoxLocation locationData) {
292         State result = UnDefType.UNDEF;
293         if (locationData != null) {
294             result = new PointType(new DecimalType(locationData.getLatitude()),
295                     new DecimalType(locationData.getLongitude()), new DecimalType(locationData.getHeight()));
296         }
297         return result;
298     }
299 }