2 * Copyright (c) 2010-2024 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, () -> connection.reallyFetchDataFromServer(senseBoxId));
106 updateStatus(ThingStatus.UNKNOWN);
107 startAutomaticRefresh();
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, offlineReason);
111 logger.debug("Thing {} initialized {}", getThing().getUID(), getThing().getStatus());
115 public void handleCommand(ChannelUID channelUID, Command command) {
116 if (command instanceof RefreshType) {
118 if (data != null && ThingStatus.ONLINE == data.getStatus()) {
119 publishDataForChannel(channelUID);
121 updateStatus(ThingStatus.ONLINE);
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
126 logger.debug("Unsupported command {}! Supported commands: REFRESH", command);
131 public void dispose() {
132 stopAutomaticRefresh();
135 private void stopAutomaticRefresh() {
136 if (refreshJob != null) {
137 refreshJob.cancel(true);
141 private void publishData() {
142 logger.debug("Refreshing data for box {}, scheduled after {} seconds...", thingConfiguration.getSenseBoxId(),
143 thingConfiguration.getRefreshInterval());
145 if (data != null && ThingStatus.ONLINE == data.getStatus()) {
148 updateStatus(ThingStatus.ONLINE);
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
154 private void startAutomaticRefresh() {
155 stopAutomaticRefresh();
156 refreshJob = scheduler.scheduleWithFixedDelay(this::publishData, 0, thingConfiguration.getRefreshInterval(),
160 private @Nullable SenseBoxData fetchData() {
161 return cache.get(CACHE_KEY_DATA);
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);
176 private void publishChannels() {
177 thing.getChannels().forEach(channel -> publishDataForChannel(channel.getUID()));
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()));
187 case CHANNEL_UV_INTENSITY:
188 updateState(channelUID, decimalFromSensor(localData.getUvIntensity()));
190 case CHANNEL_ILLUMINANCE:
191 updateState(channelUID, decimalFromSensor(localData.getLuminance()));
193 case CHANNEL_PRESSURE:
194 updateState(channelUID, decimalFromSensor(localData.getPressure()));
196 case CHANNEL_HUMIDITY:
197 updateState(channelUID, decimalFromSensor(localData.getHumidity()));
199 case CHANNEL_TEMPERATURE:
200 updateState(channelUID, decimalFromSensor(localData.getTemperature()));
202 case CHANNEL_PARTICULATE_MATTER_2_5:
203 updateState(channelUID, decimalFromSensor(localData.getParticulateMatter2dot5()));
205 case CHANNEL_PARTICULATE_MATTER_10:
206 updateState(channelUID, decimalFromSensor(localData.getParticulateMatter10()));
208 case CHANNEL_UV_INTENSITY_LR:
209 updateState(channelUID, dateTimeFromSensor(localData.getUvIntensity()));
211 case CHANNEL_ILLUMINANCE_LR:
212 updateState(channelUID, dateTimeFromSensor(localData.getLuminance()));
214 case CHANNEL_PRESSURE_LR:
215 updateState(channelUID, dateTimeFromSensor(localData.getPressure()));
217 case CHANNEL_HUMIDITY_LR:
218 updateState(channelUID, dateTimeFromSensor(localData.getHumidity()));
220 case CHANNEL_TEMPERATURE_LR:
221 updateState(channelUID, dateTimeFromSensor(localData.getTemperature()));
223 case CHANNEL_PARTICULATE_MATTER_2_5_LR:
224 updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter2dot5()));
226 case CHANNEL_PARTICULATE_MATTER_10_LR:
227 updateState(channelUID, dateTimeFromSensor(localData.getParticulateMatter10()));
230 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
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());
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()) {
256 result = new QuantityType<>(bd, Units.PERCENT);
259 result = new QuantityType<>(bd, SIUnits.CELSIUS);
262 result = new QuantityType<>(bd, SIUnits.PASCAL);
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);
269 result = new QuantityType<>(bd, MetricPrefix.HECTO(SIUnits.PASCAL));
272 result = new QuantityType<>(bd, Units.LUX);
275 result = new QuantityType<>(bd, Units.MICROGRAM_PER_CUBICMETRE);
278 result = new QuantityType<>(bd, Units.MICROWATT_PER_SQUARE_CENTIMETRE);
281 // The data provider might have configured some unknown unit, accept at least the
283 logger.debug("Could not determine unit for '{}', using default", sensorData.getUnit());
284 result = new QuantityType<>(bd, Units.ONE);
286 logger.debug("State: '{}'", result);
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()));