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);
55 private static final Duration RETRY_INIT_DELAY = Duration.ofSeconds(10);
57 private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
58 private final SerialPortManager serialPortManager;
60 private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
61 private @Nullable SDS011Communicator communicator;
63 private @Nullable ScheduledFuture<?> dataReadJob;
64 private @Nullable ScheduledFuture<?> connectionMonitor;
65 private @Nullable ScheduledFuture<?> retryInitJob;
67 private ZonedDateTime lastCommunication = ZonedDateTime.now();
69 // initialize timeBetweenDataShouldArrive with a large number
70 private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
71 private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
73 // cached values for refresh command
74 private State statePM10 = UnDefType.UNDEF;
75 private State statePM25 = UnDefType.UNDEF;
77 public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
79 this.serialPortManager = serialPortManager;
83 public void handleCommand(ChannelUID channelUID, Command command) {
84 // refresh channels with last received values from cache
85 if (RefreshType.REFRESH.equals(command)) {
86 if (NovaFineDustBindingConstants.CHANNEL_PM25.equals(channelUID.getId()) && statePM25 != UnDefType.UNDEF) {
87 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
89 if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
90 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
96 public void initialize() {
97 updateStatus(ThingStatus.UNKNOWN);
99 config = getConfigAs(NovaFineDustConfiguration.class);
101 if (!validateConfiguration()) {
105 // parse port and if the port is found, initialize the reader
106 SerialPortIdentifier portId = serialPortManager.getIdentifier(config.port);
107 if (portId == null) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known!");
109 logger.debug("Serial port {} was not found, retrying in {}.", config.port, RETRY_INIT_DELAY);
110 retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS);
114 this.communicator = new SDS011Communicator(this, portId, scheduler);
116 if (config.reporting) {
117 timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
118 scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
120 timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
121 scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
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 "Communicator instance is null in initializeCommunicator()");
133 boolean initSuccessful = false;
136 // sometimes the device is a little difficult and needs multiple configuration attempts
137 while (!initSuccessful && retryCount < retryInit) {
138 logger.trace("Trying to initialize device attempt={}", retryCount);
139 initSuccessful = doInit(localCommunicator, mode, interval);
143 if (initSuccessful) {
144 lastCommunication = ZonedDateTime.now();
145 updateStatus(ThingStatus.ONLINE);
147 if (mode == WorkMode.POLLING) {
148 dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
150 localCommunicator.requestSensorData();
151 } catch (IOException e) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
153 "Cannot query data from device");
155 }, 2, config.pollingInterval, TimeUnit.SECONDS);
157 // start a job that reads the port until data arrives
158 int reportingReadStartDelay = 10;
159 int startReadBeforeDataArrives = 5;
160 long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay
161 - startReadBeforeDataArrives;
162 logger.trace("Scheduling job to receive reported values");
163 dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
165 localCommunicator.readSensorData();
166 } catch (IOException e) {
167 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
168 "Cannot query data from device, because: " + e.getMessage());
170 }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS);
173 Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive
174 .plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
175 connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
176 connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(),
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
180 "Commands and replies from the device don't seem to match");
182 "Could not configure sensor -> setting Thing to OFFLINE, disposing the handler and reschedule initialize in {} seconds",
185 retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS);
189 private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) {
190 boolean initSuccessful = false;
192 initSuccessful = localCommunicator.initialize(mode, interval);
193 } catch (final IOException ex) {
194 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
195 } catch (PortInUseException e) {
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
197 } catch (TooManyListenersException e) {
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
199 "Cannot attach listener to port, because there are too many listeners!");
200 } catch (UnsupportedCommOperationException e) {
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
202 "Cannot set serial port parameters");
204 return initSuccessful;
207 private boolean validateConfiguration() {
208 if (config.port.isEmpty()) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
212 if (config.reporting) {
213 if (config.reportingInterval < 0 || config.reportingInterval > 30) {
214 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
215 "Reporting interval has to be between 0 and 30 minutes");
219 if (config.pollingInterval < 3 || config.pollingInterval > 3600) {
220 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
221 "Polling interval has to be between 3 and 3600 seconds");
229 public void dispose() {
233 private void doDispose(boolean sendDeviceToSleep) {
234 ScheduledFuture<?> localPollingJob = this.dataReadJob;
235 if (localPollingJob != null) {
236 localPollingJob.cancel(true);
237 this.dataReadJob = null;
240 ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
241 if (localConnectionMonitor != null) {
242 localConnectionMonitor.cancel(true);
243 this.connectionMonitor = null;
246 ScheduledFuture<?> localRetryOpenPortJob = this.retryInitJob;
247 if (localRetryOpenPortJob != null) {
248 localRetryOpenPortJob.cancel(true);
249 this.retryInitJob = null;
252 SDS011Communicator localCommunicator = this.communicator;
253 if (localCommunicator != null) {
254 localCommunicator.dispose(sendDeviceToSleep);
257 this.statePM10 = UnDefType.UNDEF;
258 this.statePM25 = UnDefType.UNDEF;
262 * Pass the data from the device to the Thing channels
264 * @param sensorData the parsed data from the sensor
266 public void updateChannels(SensorMeasuredDataReply sensorData) {
267 if (sensorData.isValidData()) {
268 logger.debug("Updating channels with data: {}", sensorData);
270 QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
271 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
272 this.statePM10 = statePM10;
274 QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
275 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
276 this.statePM25 = statePM25;
278 updateStatus(ThingStatus.ONLINE);
280 // there was a communication, even if the data was not valid, thus resetting the value here
281 lastCommunication = ZonedDateTime.now();
284 private void verifyIfStillConnected() {
285 ZonedDateTime now = ZonedDateTime.now();
286 ZonedDateTime lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTolerance);
287 if (now.isAfter(lastData)) {
288 logger.debug("Check Alive timer: Timeout: lastCommunication={}, interval={}, tollerance={}",
289 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
290 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
291 "Check connection cable and afterwards disable and enable this thing to make it work again");
292 // in case someone has pulled the plug, we dispose ourselves and the user has to deactivate/activate the
293 // thing once the cable is plugged in again
296 logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
297 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
302 * Set the firmware property on the Thing
304 * @param firmwareVersion the firmware version as a String
306 public void setFirmware(String firmwareVersion) {
307 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);