2 * Copyright (c) 2010-2021 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.novafinedust.internal;
15 import java.io.IOException;
16 import java.time.Duration;
17 import java.time.ZonedDateTime;
18 import java.util.TooManyListenersException;
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.novafinedust.internal.sds011protocol.SDS011Communicator;
25 import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode;
26 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply;
27 import org.openhab.core.io.transport.serial.PortInUseException;
28 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
29 import org.openhab.core.io.transport.serial.SerialPortManager;
30 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
31 import org.openhab.core.library.dimension.Density;
32 import org.openhab.core.library.types.QuantityType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * The {@link SDS011Handler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Stefan Triller - Initial contribution
53 public class SDS011Handler extends BaseThingHandler {
54 private static final Duration CONNECTION_MONITOR_START_DELAY_OFFSET = Duration.ofSeconds(10);
56 private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
57 private final SerialPortManager serialPortManager;
59 private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
60 private @Nullable SDS011Communicator communicator;
62 private @Nullable ScheduledFuture<?> pollingJob;
63 private @Nullable ScheduledFuture<?> connectionMonitor;
65 private ZonedDateTime lastCommunication = ZonedDateTime.now();
67 // initialize timeBetweenDataShouldArrive with a large number
68 private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
69 private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
71 // cached values for refresh command
72 private State statePM10 = UnDefType.UNDEF;
73 private State statePM25 = UnDefType.UNDEF;
75 public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
77 this.serialPortManager = serialPortManager;
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 // refresh channels with last received values from cache
83 if (RefreshType.REFRESH.equals(command)) {
84 if (NovaFineDustBindingConstants.CHANNEL_PM25.equals(channelUID.getId()) && statePM25 != UnDefType.UNDEF) {
85 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
87 if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
88 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
94 public void initialize() {
95 updateStatus(ThingStatus.UNKNOWN);
97 config = getConfigAs(NovaFineDustConfiguration.class);
99 if (!validateConfiguration()) {
103 // parse ports and if the port is found, initialize the reader
104 SerialPortIdentifier portId = serialPortManager.getIdentifier(config.port);
105 if (portId == null) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known!");
110 this.communicator = new SDS011Communicator(this, portId);
112 if (config.reporting) {
113 timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
114 scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
116 timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
117 scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
120 Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
121 connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
122 connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS);
125 private void initializeCommunicator(WorkMode mode, Duration interval) {
126 SDS011Communicator localCommunicator = communicator;
127 if (localCommunicator == null) {
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
129 "Could not create communicator instance");
133 boolean initSuccessful = false;
135 initSuccessful = localCommunicator.initialize(mode, interval);
136 } catch (final IOException ex) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
139 } catch (PortInUseException e) {
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
142 } catch (TooManyListenersException e) {
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
144 "Cannot attach listener to port!");
146 } catch (UnsupportedCommOperationException e) {
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
148 "Cannot set serial port parameters");
152 if (initSuccessful) {
153 lastCommunication = ZonedDateTime.now();
154 updateStatus(ThingStatus.ONLINE);
156 if (mode == WorkMode.POLLING) {
157 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
159 localCommunicator.requestSensorData();
160 } catch (IOException e) {
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
162 "Cannot query data from device");
164 }, 2, config.pollingInterval, TimeUnit.SECONDS);
167 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
168 "Commands and replies from the device don't seem to match");
169 logger.debug("Could not configure sensor -> setting Thing to OFFLINE and disposing the handler");
174 private boolean validateConfiguration() {
175 if (config.port.isEmpty()) {
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
179 if (config.reporting) {
180 if (config.reportingInterval < 0 || config.reportingInterval > 30) {
181 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
182 "Reporting interval has to be between 0 and 30 minutes");
186 if (config.pollingInterval < 3 || config.pollingInterval > 3600) {
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
188 "Polling interval has to be between 3 and 3600 seconds");
196 public void dispose() {
197 ScheduledFuture<?> localPollingJob = this.pollingJob;
198 if (localPollingJob != null) {
199 localPollingJob.cancel(true);
200 this.pollingJob = null;
203 ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
204 if (localConnectionMonitor != null) {
205 localConnectionMonitor.cancel(true);
206 this.connectionMonitor = null;
209 SDS011Communicator localCommunicator = this.communicator;
210 if (localCommunicator != null) {
211 localCommunicator.dispose();
214 this.statePM10 = UnDefType.UNDEF;
215 this.statePM25 = UnDefType.UNDEF;
219 * Pass the data from the device to the Thing channels
221 * @param sensorData the parsed data from the sensor
223 public void updateChannels(SensorMeasuredDataReply sensorData) {
224 if (sensorData.isValidData()) {
225 logger.debug("Updating channels with data: {}", sensorData);
227 QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
228 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
229 this.statePM10 = statePM10;
231 QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
232 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
233 this.statePM25 = statePM25;
235 updateStatus(ThingStatus.ONLINE);
237 // there was a communication, even if the data was not valid, thus resetting the value here
238 lastCommunication = ZonedDateTime.now();
241 private void verifyIfStillConnected() {
242 ZonedDateTime now = ZonedDateTime.now();
243 ZonedDateTime lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTolerance);
244 if (now.isAfter(lastData)) {
245 logger.debug("Check Alive timer: Timeout: lastCommunication={}, interval={}, tollerance={}",
246 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
248 "Check connection cable and afterwards disable and enable this thing to make it work again");
249 // in case someone has pulled the plug, we dispose ourselves and the user has to deactivate/activate the
250 // thing once the cable is plugged in again
253 logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
254 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
259 * Set the firmware property on the Thing
261 * @param firmwareVersion the firmware version as a String
263 public void setFirmware(String firmwareVersion) {
264 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);