]> git.basschouten.com Git - openhab-addons.git/blob
87cec1ad18bc2a2a8e4c7eac1870f6234c7e06f5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
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;
45
46 /**
47  * The {@link SDS011Handler} is responsible for handling commands, which are
48  * sent to one of the channels.
49  *
50  * @author Stefan Triller - Initial contribution
51  */
52 @NonNullByDefault
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);
56
57     private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
58     private final SerialPortManager serialPortManager;
59
60     private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
61     private @Nullable SDS011Communicator communicator;
62
63     private @Nullable ScheduledFuture<?> dataReadJob;
64     private @Nullable ScheduledFuture<?> connectionMonitor;
65     private @Nullable ScheduledFuture<?> retryInitJob;
66
67     private ZonedDateTime lastCommunication = ZonedDateTime.now();
68
69     // initialize timeBetweenDataShouldArrive with a large number
70     private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
71     private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
72
73     // cached values for refresh command
74     private State statePM10 = UnDefType.UNDEF;
75     private State statePM25 = UnDefType.UNDEF;
76
77     public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
78         super(thing);
79         this.serialPortManager = serialPortManager;
80     }
81
82     @Override
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);
88             }
89             if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
90                 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
91             }
92         }
93     }
94
95     @Override
96     public void initialize() {
97         updateStatus(ThingStatus.UNKNOWN);
98
99         config = getConfigAs(NovaFineDustConfiguration.class);
100
101         if (!validateConfiguration()) {
102             return;
103         }
104
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);
111             return;
112         }
113
114         this.communicator = new SDS011Communicator(this, portId, scheduler);
115
116         if (config.reporting) {
117             timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
118             scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
119         } else {
120             timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
121             scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
122         }
123     }
124
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()");
130             return;
131         }
132
133         boolean initSuccessful = false;
134         int retryInit = 3;
135         int retryCount = 0;
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);
140             retryCount++;
141         }
142
143         if (initSuccessful) {
144             lastCommunication = ZonedDateTime.now();
145             updateStatus(ThingStatus.ONLINE);
146
147             if (mode == WorkMode.POLLING) {
148                 dataReadJob = scheduler.scheduleWithFixedDelay(() -> {
149                     try {
150                         localCommunicator.requestSensorData();
151                     } catch (IOException e) {
152                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
153                                 "Cannot query data from device");
154                     }
155                 }, 2, config.pollingInterval, TimeUnit.SECONDS);
156             } else {
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(() -> {
164                     try {
165                         localCommunicator.readSensorData();
166                     } catch (IOException e) {
167                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
168                                 "Cannot query data from device, because: " + e.getMessage());
169                     }
170                 }, reportingReadStartDelay, readReportedDataInterval, TimeUnit.SECONDS);
171             }
172
173             Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive
174                     .plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
175             connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
176                     connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(),
177                     TimeUnit.SECONDS);
178         } else {
179             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
180                     "Commands and replies from the device don't seem to match");
181             logger.debug(
182                     "Could not configure sensor -> setting Thing to OFFLINE, disposing the handler and reschedule initialize in {} seconds",
183                     RETRY_INIT_DELAY);
184             doDispose(false);
185             retryInitJob = scheduler.schedule(this::initialize, RETRY_INIT_DELAY.getSeconds(), TimeUnit.SECONDS);
186         }
187     }
188
189     private boolean doInit(SDS011Communicator localCommunicator, WorkMode mode, Duration interval) {
190         boolean initSuccessful = false;
191         try {
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");
203         }
204         return initSuccessful;
205     }
206
207     private boolean validateConfiguration() {
208         if (config.port.isEmpty()) {
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
210             return false;
211         }
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");
216                 return false;
217             }
218         } else {
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");
222                 return false;
223             }
224         }
225         return true;
226     }
227
228     @Override
229     public void dispose() {
230         doDispose(true);
231     }
232
233     private void doDispose(boolean sendDeviceToSleep) {
234         ScheduledFuture<?> localPollingJob = this.dataReadJob;
235         if (localPollingJob != null) {
236             localPollingJob.cancel(true);
237             this.dataReadJob = null;
238         }
239
240         ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
241         if (localConnectionMonitor != null) {
242             localConnectionMonitor.cancel(true);
243             this.connectionMonitor = null;
244         }
245
246         ScheduledFuture<?> localRetryOpenPortJob = this.retryInitJob;
247         if (localRetryOpenPortJob != null) {
248             localRetryOpenPortJob.cancel(true);
249             this.retryInitJob = null;
250         }
251
252         SDS011Communicator localCommunicator = this.communicator;
253         if (localCommunicator != null) {
254             localCommunicator.dispose(sendDeviceToSleep);
255         }
256
257         this.statePM10 = UnDefType.UNDEF;
258         this.statePM25 = UnDefType.UNDEF;
259     }
260
261     /**
262      * Pass the data from the device to the Thing channels
263      *
264      * @param sensorData the parsed data from the sensor
265      */
266     public void updateChannels(SensorMeasuredDataReply sensorData) {
267         if (sensorData.isValidData()) {
268             logger.debug("Updating channels with data: {}", sensorData);
269
270             QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
271             updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
272             this.statePM10 = statePM10;
273
274             QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
275             updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
276             this.statePM25 = statePM25;
277
278             updateStatus(ThingStatus.ONLINE);
279         }
280         // there was a communication, even if the data was not valid, thus resetting the value here
281         lastCommunication = ZonedDateTime.now();
282     }
283
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
294             doDispose(false);
295         } else {
296             logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
297                     lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
298         }
299     }
300
301     /**
302      * Set the firmware property on the Thing
303      *
304      * @param firmwareVersion the firmware version as a String
305      */
306     public void setFirmware(String firmwareVersion) {
307         updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
308     }
309 }