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.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.Future;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.novafinedust.internal.sds011protocol.SDS011Communicator;
26 import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode;
27 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply;
28 import org.openhab.core.io.transport.serial.PortInUseException;
29 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
30 import org.openhab.core.io.transport.serial.SerialPortManager;
31 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
32 import org.openhab.core.library.dimension.Density;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.unit.Units;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link SDS011Handler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Stefan Triller - Initial contribution
54 public class SDS011Handler extends BaseThingHandler {
55 private static final Duration CONNECTION_MONITOR_START_DELAY_OFFSET = Duration.ofSeconds(10);
56 private static final Duration RETRY_INIT_DELAY = Duration.ofSeconds(10);
58 private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
59 private final SerialPortManager serialPortManager;
61 private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
62 private @Nullable SDS011Communicator communicator;
64 private @Nullable ScheduledFuture<?> dataReadJob;
65 private @Nullable ScheduledFuture<?> connectionMonitor;
66 private @Nullable Future<?> initJob;
67 private @Nullable ScheduledFuture<?> retryInitJob;
69 private ZonedDateTime lastCommunication = ZonedDateTime.now();
71 // initialize timeBetweenDataShouldArrive with a large number
72 private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
73 private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
75 // cached values for refresh command
76 private State statePM10 = UnDefType.UNDEF;
77 private State statePM25 = UnDefType.UNDEF;
79 public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
81 this.serialPortManager = serialPortManager;
85 public void handleCommand(ChannelUID channelUID, Command command) {
86 // refresh channels with last received values from cache
87 if (RefreshType.REFRESH.equals(command)) {
88 if (NovaFineDustBindingConstants.CHANNEL_PM25.equals(channelUID.getId()) && statePM25 != UnDefType.UNDEF) {
89 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
91 if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
92 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
98 public void initialize() {
99 updateStatus(ThingStatus.UNKNOWN);
101 config = getConfigAs(NovaFineDustConfiguration.class);
103 if (!validateConfiguration()) {
107 // parse port and if the port is found, initialize the reader
108 SerialPortIdentifier portId = serialPortManager.getIdentifier(config.port);
109 if (portId == null) {
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known!");
111 logger.debug("Serial port {} was not found, retrying in {}.", config.port, RETRY_INIT_DELAY);
112 retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS);
116 this.communicator = new SDS011Communicator(this, portId, scheduler);
118 if (config.reporting) {
119 timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
120 initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
122 timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
123 initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
127 private void initializeCommunicator(WorkMode mode, Duration interval) {
128 SDS011Communicator localCommunicator = communicator;
129 if (localCommunicator == null) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
131 "Communicator instance is null in initializeCommunicator()");
135 logger.trace("Trying to initialize device");
136 doInit(localCommunicator, mode, interval);
138 lastCommunication = ZonedDateTime.now();
140 if (mode == WorkMode.POLLING) {
141 dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
143 localCommunicator.requestSensorData();
144 } catch (IOException e) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
146 "Cannot query data from device");
148 }, 2, config.pollingInterval, TimeUnit.SECONDS);
150 // start a job that reads the port until data arrives
151 int reportingReadStartDelay = 10;
152 int startReadBeforeDataArrives = 5;
153 long readReportedDataInterval = (config.reportingInterval * 60) - reportingReadStartDelay
154 - startReadBeforeDataArrives;
155 logger.trace("Scheduling job to receive reported values");
156 dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
158 localCommunicator.readSensorData();
159 } catch (IOException e) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
161 "Cannot query data from device, because: " + e.getMessage());
163 }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS);
166 Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
167 connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
168 connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS);
171 private void doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) {
173 localCommunicator.initialize(mode, interval);
174 } catch (final IOException ex) {
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
176 } catch (PortInUseException e) {
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
178 } catch (TooManyListenersException e) {
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
180 "Cannot attach listener to port, because there are too many listeners!");
181 } catch (UnsupportedCommOperationException e) {
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
183 "Cannot set serial port parameters");
187 private boolean validateConfiguration() {
188 if (config.port.isEmpty()) {
189 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
192 if (config.reporting) {
193 if (config.reportingInterval < 0 || config.reportingInterval > 30) {
194 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
195 "Reporting interval has to be between 0 and 30 minutes");
199 if (config.pollingInterval < 3 || config.pollingInterval > 3600) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
201 "Polling interval has to be between 3 and 3600 seconds");
209 public void dispose() {
213 private void doDispose(boolean sendDeviceToSleep) {
214 ScheduledFuture<?> localPollingJob = this.dataReadJob;
215 if (localPollingJob != null) {
216 localPollingJob.cancel(true);
217 this.dataReadJob = null;
220 ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
221 if (localConnectionMonitor != null) {
222 localConnectionMonitor.cancel(true);
223 this.connectionMonitor = null;
226 Future<?> localInitJob = this.initJob;
227 if (localInitJob != null) {
228 localInitJob.cancel(true);
232 ScheduledFuture<?> localRetryOpenPortJob = this.retryInitJob;
233 if (localRetryOpenPortJob != null) {
234 localRetryOpenPortJob.cancel(true);
235 this.retryInitJob = null;
238 SDS011Communicator localCommunicator = this.communicator;
239 if (localCommunicator != null) {
240 localCommunicator.dispose(sendDeviceToSleep);
243 this.statePM10 = UnDefType.UNDEF;
244 this.statePM25 = UnDefType.UNDEF;
248 * Pass the data from the device to the Thing channels
250 * @param sensorData the parsed data from the sensor
252 public void updateChannels(SensorMeasuredDataReply sensorData) {
253 if (sensorData.isValidData()) {
254 logger.debug("Updating channels with data: {}", sensorData);
256 QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
257 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
258 this.statePM10 = statePM10;
260 QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
261 updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
262 this.statePM25 = statePM25;
264 updateStatus(ThingStatus.ONLINE);
266 // there was a communication, even if the data was not valid, thus resetting the value here
267 lastCommunication = ZonedDateTime.now();
270 private void verifyIfStillConnected() {
271 ZonedDateTime now = ZonedDateTime.now();
272 ZonedDateTime lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTolerance);
273 if (now.isAfter(lastData)) {
274 logger.debug("Check Alive timer: Timeout: lastCommunication={}, interval={}, tollerance={}",
275 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
276 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
277 "Check connection cable and afterwards disable and enable this thing to make it work again");
278 // in case someone has pulled the plug, we dispose ourselves and the user has to deactivate/activate the
279 // thing once the cable is plugged in again
282 logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
283 lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
288 * Set the firmware property on the Thing
290 * @param firmwareVersion the firmware version as a String
292 public void setFirmware(String firmwareVersion) {
293 updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);