]> git.basschouten.com Git - openhab-addons.git/blob
656896215daf26e5c440ea9ddcbba24de9875471
[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
56     private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class);
57     private final SerialPortManager serialPortManager;
58
59     private NovaFineDustConfiguration config = new NovaFineDustConfiguration();
60     private @Nullable SDS011Communicator communicator;
61
62     private @Nullable ScheduledFuture<?> pollingJob;
63     private @Nullable ScheduledFuture<?> connectionMonitor;
64
65     private ZonedDateTime lastCommunication = ZonedDateTime.now();
66
67     // initialize timeBetweenDataShouldArrive with a large number
68     private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
69     private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5);
70
71     // cached values for refresh command
72     private State statePM10 = UnDefType.UNDEF;
73     private State statePM25 = UnDefType.UNDEF;
74
75     public SDS011Handler(Thing thing, SerialPortManager serialPortManager) {
76         super(thing);
77         this.serialPortManager = serialPortManager;
78     }
79
80     @Override
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);
86             }
87             if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) {
88                 updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
89             }
90         }
91     }
92
93     @Override
94     public void initialize() {
95         updateStatus(ThingStatus.UNKNOWN);
96
97         config = getConfigAs(NovaFineDustConfiguration.class);
98
99         if (!validateConfiguration()) {
100             return;
101         }
102
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!");
107             return;
108         }
109
110         this.communicator = new SDS011Communicator(this, portId);
111
112         if (config.reporting) {
113             timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval);
114             scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));
115         } else {
116             timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval);
117             scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));
118         }
119
120         Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET);
121         connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected,
122                 connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS);
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                     "Could not create communicator instance");
130             return;
131         }
132
133         boolean initSuccessful = false;
134         try {
135             initSuccessful = localCommunicator.initialize(mode, interval);
136         } catch (final IOException ex) {
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
138             return;
139         } catch (PortInUseException e) {
140             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
141             return;
142         } catch (TooManyListenersException e) {
143             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
144                     "Cannot attach listener to port!");
145             return;
146         } catch (UnsupportedCommOperationException e) {
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
148                     "Cannot set serial port parameters");
149             return;
150         }
151
152         if (initSuccessful) {
153             lastCommunication = ZonedDateTime.now();
154             updateStatus(ThingStatus.ONLINE);
155
156             if (mode == WorkMode.POLLING) {
157                 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
158                     try {
159                         localCommunicator.requestSensorData();
160                     } catch (IOException e) {
161                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
162                                 "Cannot query data from device");
163                     }
164                 }, 2, config.pollingInterval, TimeUnit.SECONDS);
165             }
166         } else {
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");
170             dispose();
171         }
172     }
173
174     private boolean validateConfiguration() {
175         if (config.port.isEmpty()) {
176             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
177             return false;
178         }
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");
183                 return false;
184             }
185         } else {
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");
189                 return false;
190             }
191         }
192         return true;
193     }
194
195     @Override
196     public void dispose() {
197         ScheduledFuture<?> localPollingJob = this.pollingJob;
198         if (localPollingJob != null) {
199             localPollingJob.cancel(true);
200             this.pollingJob = null;
201         }
202
203         ScheduledFuture<?> localConnectionMonitor = this.connectionMonitor;
204         if (localConnectionMonitor != null) {
205             localConnectionMonitor.cancel(true);
206             this.connectionMonitor = null;
207         }
208
209         SDS011Communicator localCommunicator = this.communicator;
210         if (localCommunicator != null) {
211             localCommunicator.dispose();
212         }
213
214         this.statePM10 = UnDefType.UNDEF;
215         this.statePM25 = UnDefType.UNDEF;
216     }
217
218     /**
219      * Pass the data from the device to the Thing channels
220      *
221      * @param sensorData the parsed data from the sensor
222      */
223     public void updateChannels(SensorMeasuredDataReply sensorData) {
224         if (sensorData.isValidData()) {
225             logger.debug("Updating channels with data: {}", sensorData);
226
227             QuantityType<Density> statePM10 = new QuantityType<>(sensorData.getPm10(), Units.MICROGRAM_PER_CUBICMETRE);
228             updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
229             this.statePM10 = statePM10;
230
231             QuantityType<Density> statePM25 = new QuantityType<>(sensorData.getPm25(), Units.MICROGRAM_PER_CUBICMETRE);
232             updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25);
233             this.statePM25 = statePM25;
234
235             updateStatus(ThingStatus.ONLINE);
236         }
237         // there was a communication, even if the data was not valid, thus resetting the value here
238         lastCommunication = ZonedDateTime.now();
239     }
240
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
251             dispose();
252         } else {
253             logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}",
254                     lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance);
255         }
256     }
257
258     /**
259      * Set the firmware property on the Thing
260      *
261      * @param firmwareVersion the firmware version as a String
262      */
263     public void setFirmware(String firmwareVersion) {
264         updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
265     }
266 }