]> git.basschouten.com Git - openhab-addons.git/blob
5de8002de99d2bf446ae9c448e88a0a59bb3d844
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.novafinedust.internal;
14
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;
22
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;
46
47 /**
48  * The {@link SDS011Handler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Stefan Triller - Initial contribution
52  */
53 @NonNullByDefault
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);
57
58     private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
59     private final SerialPortManager serialPortManager;
60
61     private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
62     private @Nullable SDS011Communicator communicator;
63
64     private @Nullable ScheduledFuture<?> dataReadJob;
65     private @Nullable ScheduledFuture<?> connectionMonitor;
66     private @Nullable Future<?> initJob;
67     private @Nullable ScheduledFuture<?> retryInitJob;
68
69     private ZonedDateTime lastCommunication = ZonedDateTime.now();
70
71     // initialize timeBetweenDataShouldArrive with a large number
72     private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
73     private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
74
75     // cached values for refresh command
76     private State statePM10 = UnDefType.UNDEF;
77     private State statePM25 = UnDefType.UNDEF;
78
79     public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
80         super(thing);
81         this.serialPortManager = serialPortManager;
82     }
83
84     @Override
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);
90             }
91             if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
92                 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
93             }
94         }
95     }
96
97     @Override
98     public void initialize() {
99         updateStatus(ThingStatus.UNKNOWN);
100
101         config = getConfigAs(NovaFineDustConfiguration.class);
102
103         if (!validateConfiguration()) {
104             return;
105         }
106
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);
113             return;
114         }
115
116         this.communicator = new SDS011Communicator(this, portId, scheduler);
117
118         if (config.reporting) {
119             timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
120             initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
121         } else {
122             timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
123             initJob = scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
124         }
125     }
126
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()");
132             return;
133         }
134
135         logger.trace("Trying to initialize device");
136         doInit(localCommunicator, mode, interval);
137
138         lastCommunication = ZonedDateTime.now();
139
140         if (mode == WorkMode.POLLING) {
141             dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
142                 try {
143                     localCommunicator.requestSensorData();
144                 } catch (IOException e) {
145                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
146                             "Cannot query data from device");
147                 }
148             }, 2, config.pollingInterval, TimeUnit.SECONDS);
149         } else {
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(() -> {
157                 try {
158                     localCommunicator.readSensorData();
159                 } catch (IOException e) {
160                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
161                             "Cannot query data from device, because: " + e.getMessage());
162                 }
163             }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS);
164         }
165
166         Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
167         connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
168                 connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS);
169     }
170
171     private void doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) {
172         try {
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");
184         }
185     }
186
187     private boolean validateConfiguration() {
188         if (config.port.isEmpty()) {
189             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
190             return false;
191         }
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");
196                 return false;
197             }
198         } else {
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");
202                 return false;
203             }
204         }
205         return true;
206     }
207
208     @Override
209     public void dispose() {
210         doDispose(true);
211     }
212
213     private void doDispose(boolean sendDeviceToSleep) {
214         ScheduledFuture<?> localPollingJob = this.dataReadJob;
215         if (localPollingJob != null) {
216             localPollingJob.cancel(true);
217             this.dataReadJob = null;
218         }
219
220         ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
221         if (localConnectionMonitor != null) {
222             localConnectionMonitor.cancel(true);
223             this.connectionMonitor = null;
224         }
225
226         Future<?> localInitJob = this.initJob;
227         if (localInitJob != null) {
228             localInitJob.cancel(true);
229             this.initJob = null;
230         }
231
232         ScheduledFuture<?> localRetryOpenPortJob = this.retryInitJob;
233         if (localRetryOpenPortJob != null) {
234             localRetryOpenPortJob.cancel(true);
235             this.retryInitJob = null;
236         }
237
238         SDS011Communicator localCommunicator = this.communicator;
239         if (localCommunicator != null) {
240             localCommunicator.dispose(sendDeviceToSleep);
241         }
242
243         this.statePM10 = UnDefType.UNDEF;
244         this.statePM25 = UnDefType.UNDEF;
245     }
246
247     /**
248      * Pass the data from the device to the Thing channels
249      *
250      * @param sensorData the parsed data from the sensor
251      */
252     public void updateChannels(SensorMeasuredDataReply sensorData) {
253         if (sensorData.isValidData()) {
254             logger.debug("Updating channels with data: {}", sensorData);
255
256             QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
257             updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
258             this.statePM10 = statePM10;
259
260             QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
261             updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
262             this.statePM25 = statePM25;
263
264             updateStatus(ThingStatus.ONLINE);
265         }
266         // there was a communication, even if the data was not valid, thus resetting the value here
267         lastCommunication = ZonedDateTime.now();
268     }
269
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
280             doDispose(false);
281         } else {
282             logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
283                     lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
284         }
285     }
286
287     /**
288      * Set the firmware property on the Thing
289      *
290      * @param firmwareVersion the firmware version as a String
291      */
292     public void setFirmware(String firmwareVersion) {
293         updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
294     }
295 }