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.sensebox.internal.handler;
15 import static org.openhab.binding.sensebox.internal.SenseBoxBindingConstants.*;
17 import java.math.BigDecimal;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
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;
50 * The {@link SenseBoxHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
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
59 public class SenseBoxHandler extends BaseThingHandler {
61 private final Logger logger = LoggerFactory.getLogger(SenseBoxHandler.class);
63 protected @NonNullByDefault({}) SenseBoxConfiguration thingConfiguration;
65 private @Nullable SenseBoxData data;
67 private @Nullable ScheduledFuture<?> refreshJob;
69 private static final BigDecimal ONEHUNDRED = BigDecimal.valueOf(100l);
71 private static final String CACHE_KEY_DATA = "DATA";
73 private final ExpiringCacheMap<String, SenseBoxData> cache = new ExpiringCacheMap<>(CACHE_EXPIRY);
75 private final SenseBoxAPIConnection connection = new SenseBoxAPIConnection();
77 public SenseBoxHandler(Thing thing) {
82 public void initialize() {
83 logger.debug("Start initializing!");
85 thingConfiguration = getConfigAs(SenseBoxConfiguration.class);
87 String senseBoxId = thingConfiguration.getSenseBoxId();
88 logger.debug("Thing Configuration {} initialized {}", getThing().getUID(), senseBoxId);
90 String offlineReason = "";
91 boolean validConfig = true;
93 if (senseBoxId == null || senseBoxId.trim().isEmpty()) {
94 offlineReason = "senseBox ID is mandatory and must be configured";
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);
104 if (senseBoxId != null && validConfig) {
105 cache.put(CACHE_KEY_DATA, () -> {
106 return connection.reallyFetchDataFromServer(senseBoxId);
108 updateStatus(ThingStatus.UNKNOWN);
109 startAutomaticRefresh();
111 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, offlineReason);
113 logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
117 public void handleCommand(ChannelUID channelUID, Command command) {
118 if (command instanceof RefreshType) {
120 if (data != null && ThingStatus.ONLINE == data.getStatus()) {
121 publishDataForChannel(channelUID);
123 updateStatus(ThingStatus.ONLINE);
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
128 logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
133 public void dispose() {
134 stopAutomaticRefresh();
137 private void stopAutomaticRefresh() {
138 if (refreshJob != null) {
139 refreshJob.cancel(true);
143 private void publishData() {
144 logger.debug("Refreshing data for box {}, scheduled after {} seconds...", thingConfiguration.getSenseBoxId(),
145 thingConfiguration.getRefreshInterval());
147 if (data != null && ThingStatus.ONLINE == data.getStatus()) {
150 updateStatus(ThingStatus.ONLINE);
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
156 private void startAutomaticRefresh() {
157 stopAutomaticRefresh();
158 refreshJob = scheduler.scheduleWithFixedDelay(this::publishData, 0, thingConfiguration.getRefreshInterval(),
162 private @Nullable SenseBoxData fetchData() {
163 return cache.get(CACHE_KEY_DATA);
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);
178 private void publishChannels() {
179 thing.getChannels().forEach(channel -> publishDataForChannel(channel.getUID()));
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()));
189 case CHANNEL_UV_INTENSITY:
190 updateState(channelUID, decimalFromSensor(localData.getUvIntensity()));
192 case CHANNEL_ILLUMINANCE:
193 updateState(channelUID, decimalFromSensor(localData.getLuminance()));
195 case CHANNEL_PRESSURE:
196 updateState(channelUID, decimalFromSensor(localData.getPressure()));
198 case CHANNEL_HUMIDITY:
199 updateState(channelUID, decimalFromSensor(localData.getHumidity()));
201 case CHANNEL_TEMPERATURE:
202 updateState(channelUID, decimalFromSensor(localData.getTemperature()));
204 case CHANNEL_PARTICULATE_MATTER_2_5:
205 updateState(channelUID, decimalFromSensor(localData.getParticulateMatter2dot5()));
207 case CHANNEL_PARTICULATE_MATTER_10:
208 updateState(channelUID, decimalFromSensor(localData.getParticulateMatter10()));
210 case CHANNEL_UV_INTENSITY_LR:
211 updateState(channelUID, dateTimeFromSensor(localData.getUvIntensity()));
213 case CHANNEL_ILLUMINANCE_LR:
214 updateState(channelUID, dateTimeFromSensor(localData.getLuminance()));
216 case CHANNEL_PRESSURE_LR:
217 updateState(channelUID, dateTimeFromSensor(localData.getPressure()));
219 case CHANNEL_HUMIDITY_LR:
220 updateState(channelUID, dateTimeFromSensor(localData.getHumidity()));
222 case CHANNEL_TEMPERATURE_LR:
223 updateState(channelUID, dateTimeFromSensor(localData.getTemperature()));
225 case CHANNEL_PARTICULATE_MATTER_2_5_LR:
226 updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter2dot5()));
228 case CHANNEL_PARTICULATE_MATTER_10_LR:
229 updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter10()));
232 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
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());
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()) {
258 result = new QuantityType<>(bd, Units.PERCENT);
261 result = new QuantityType<>(bd, SIUnits.CELSIUS);
264 result = new QuantityType<>(bd, SIUnits.PASCAL);
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);
271 result = new QuantityType<>(bd, MetricPrefix.HECTO(SIUnits.PASCAL));
274 result = new QuantityType<>(bd, Units.LUX);
277 result = new QuantityType<>(bd, Units.MICROGRAM_PER_CUBICMETRE);
280 result = new QuantityType<>(bd, Units.MICROWATT_PER_SQUARE_CENTIMETRE);
283 // The data provider might have configured some unknown unit, accept at least the
285 logger.debug("Could not determine unit for '{}', using default", sensorData.getUnit());
286 result = new QuantityType<>(bd, Units.ONE);
288 logger.debug("State: '{}'", result);
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()));