diff --git a/CODEOWNERS b/CODEOWNERS index 684247b066ee7..5841471a3c6b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -136,6 +136,7 @@ /bundles/org.openhab.binding.nibeuplink/ @alexf2015 /bundles/org.openhab.binding.nikobus/ @crnjan /bundles/org.openhab.binding.nikohomecontrol/ @mherwege +/bundles/org.openhab.binding.novafinedust/ @t2000 /bundles/org.openhab.binding.ntp/ @marcelrv /bundles/org.openhab.binding.nuki/ @mkatter /bundles/org.openhab.binding.oceanic/ @kgoderis diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 2fd594e52eaed..64ecd99fa7390 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -671,6 +671,11 @@ org.openhab.binding.nikohomecontrol ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.novafinedust + ${project.version} + org.openhab.addons.bundles org.openhab.binding.ntp diff --git a/bundles/org.openhab.binding.novafinedust/.classpath b/bundles/org.openhab.binding.novafinedust/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.novafinedust/.project b/bundles/org.openhab.binding.novafinedust/.project new file mode 100644 index 0000000000000..ce6cbf0e75027 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.novafinedust + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.novafinedust/NOTICE b/bundles/org.openhab.binding.novafinedust/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.novafinedust/README.md b/bundles/org.openhab.binding.novafinedust/README.md new file mode 100644 index 0000000000000..b5b1e8ea0b593 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/README.md @@ -0,0 +1,84 @@ +# NovaFineDust Binding + +This binding is for the fine dust sensor (PM Sensor) from Nova Fitness. +Currently only one model is supported, the SDS011. + +It basically implements the protocol specified in [this document](https://cdn.sparkfun.com/assets/parts/1/2/2/7/5/Laser_Dust_Sensor_Control_Protocol_V1.3.pdf). +One can measure the PM 2.5 and PM 10 values with this device. +It comes very handy for detecting air pollution like neighbors firing their oven with wet wood etc. so one can deactivate the ventilation system. + +## Supported Things + +There is only one Thing type for this binding, which is `SDS011`. + +## Discovery + +There is no automatic discovery. + +## Thing Configuration + +There are 2 different working modes for the `SDS011` thing: Reporting and Polling. + +### Reporting + +This is the preferred mode and thus also configured as a default. +In this mode the sensor wakes up every `reportingInterval` minutes, performs a measurement for 30 seconds and sleeps for `reportingInterval` minus 30 seconds. +Remember: According to the [datasheet](https://www-sd-nf.oss-cn-beijing.aliyuncs.com/%E5%AE%98%E7%BD%91%E4%B8%8B%E8%BD%BD/SDS011%20laser%20PM2.5%20sensor%20specification-V1.4.pdf) the sensor has a lifetime of 8000 hours. Using a 0 as `reportingInterval` will make the sensor report its data as fast as possible. + +### Polling + +If one needs data in different intervals, i.e. not as fast as possible and not in intervals that are a multiple of full minutes, polling can be configured. +The `pollingInterval` parameter specifies the time in seconds when data will be polled from the sensor. + +In addition to the mode one has to provide the port to which the device is connected. + +A full overview about the parameters of the `SDS011` thing is given in the following table: + +| parameter name | mandatory | description | +|-------------------|-----------|---------------------------------------------------------------------------------------| +| port | yes | the port the sensor is connected to, i.e. /dev/ttyUSB0. | +| reporting | no | whether the reporting mode (value=true) or polling mode should be used. | +| reportingInterval | no | the time in minutes between reportings from the sensor (default=1, min=0, max=30). | +| pollingInterval | no | the time in seconds between data polls from the device. (default=10, min=3, max=3600) | + +## Channels + +Since the supported device is a sensor, both channels are read-only channels. + +| channel | type | description | +|----------|----------------|-------------------------------| +| pm25 | Number:Density | This provides the PM2.5 value | +| pm10 | Number:Density | This provides the PM10 value | + +## Full Example + +demo.things: + +``` +Thing novafinedust:SDS011:mySDS011Report "My SDS011 Fine Dust Sensor with reporting" [ port="/dev/ttyUSB0", reporting=true, reportingInterval=1 ] +Thing novafinedust:SDS011:mySDS011Poll "My SDS011 Fine Dust Sensor with polling" [ port="/dev/ttyUSB0", reporting=false, pollingInterval=10 ] +``` + +demo.items: + +``` +Number:Density PM25 "My PM 2.5 value" { channel="novafinedust:SDS011:mySDS011Report:pm25" } +Number:Density PM10 "My PM 10 value" { channel="novafinedust:SDS011:mySDS011Report:pm10" } +``` + +demo.sitemap: + +``` +sitemap demo label="Main Menu" +{ + Frame { + Text item=PM25 label="My PM 2.5 value" + Text item=PM10 label="My PM 10 value" + } +} +``` + +## Limitations + +In theory one can have multiple sensors connected and distinguish them via their device ID. However, this is currently not implemented and the binding always configures any device and accepts data reportings from any device too. +However, it is implemented that one can attach one sensor to one serial port, like `/dev/ttyUSB0` and a second sensor on a different serial port, like `/dev/ttyUSB1`. diff --git a/bundles/org.openhab.binding.novafinedust/pom.xml b/bundles/org.openhab.binding.novafinedust/pom.xml new file mode 100644 index 0000000000000..06f7b28af079a --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/pom.xml @@ -0,0 +1,16 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.6-SNAPSHOT + + + org.openhab.binding.novafinedust + + openHAB Add-ons :: Bundles :: NovaFineDust Binding + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml b/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml new file mode 100644 index 0000000000000..0c485318fc7c4 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.novafinedust/${project.version} + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java new file mode 100644 index 0000000000000..e73629672c43d --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustBindingConstants.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link NovaFineDustBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class NovaFineDustBindingConstants { + + private static final String BINDING_ID = "novafinedust"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SDS011 = new ThingTypeUID(BINDING_ID, "SDS011"); + + // List of all Channel ids + public static final String CHANNEL_PM25 = "pm25"; + public static final String CHANNEL_PM10 = "pm10"; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java new file mode 100644 index 0000000000000..08c52a09b32b8 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustConfiguration.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link NovaFineDustConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class NovaFineDustConfiguration { + + /** + * USB port of the device + */ + public String port = ""; + public boolean reporting = true; + public int reportingInterval = 1; + public int pollingInterval = 10; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java new file mode 100644 index 0000000000000..860c4617acb99 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/NovaFineDustHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal; + +import static org.openhab.binding.novafinedust.internal.NovaFineDustBindingConstants.THING_TYPE_SDS011; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link NovaFineDustHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.novafinedust", service = ThingHandlerFactory.class) +public class NovaFineDustHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_SDS011); + + private final SerialPortManager serialPortManager; + + @Activate + public NovaFineDustHandlerFactory(@Reference SerialPortManager serialPortManager) { + this.serialPortManager = serialPortManager; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_SDS011.equals(thingTypeUID)) { + return new SDS011Handler(thing, serialPortManager); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java new file mode 100644 index 0000000000000..bf82a39267c17 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/SDS011Handler.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal; + +import java.io.IOException; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.TooManyListenersException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.dimension.Density; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +import org.openhab.binding.novafinedust.internal.sds011protocol.SDS011Communicator; +import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SDS011Handler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Stefan Triller - Initial contribution + */ +@NonNullByDefault +public class SDS011Handler extends BaseThingHandler { + private static final Duration CONNECTION_MONITOR_START_DELAY_OFFSET = Duration.ofSeconds(10); + + private final Logger logger = LoggerFactory.getLogger(SDS011Handler.class); + private final SerialPortManager serialPortManager; + + private NovaFineDustConfiguration config = new NovaFineDustConfiguration(); + private @Nullable SDS011Communicator communicator; + + private @Nullable ScheduledFuture pollingJob; + private @Nullable ScheduledFuture connectionMonitor; + + private ZonedDateTime lastCommunication = ZonedDateTime.now(); + + // initialize timeBetweenDataShouldArrive with a large number + private Duration timeBetweenDataShouldArrive = Duration.ofDays(1); + private final Duration dataCanBeLateTolerance = Duration.ofSeconds(5); + + // cached values for refresh command + private State statePM10 = UnDefType.UNDEF; + private State statePM25 = UnDefType.UNDEF; + + public SDS011Handler(Thing thing, SerialPortManager serialPortManager) { + super(thing); + this.serialPortManager = serialPortManager; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // refresh channels with last received values from cache + if (RefreshType.REFRESH.equals(command)) { + if (NovaFineDustBindingConstants.CHANNEL_PM25.equals(channelUID.getId()) && statePM25 != UnDefType.UNDEF) { + updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25); + } + if (NovaFineDustBindingConstants.CHANNEL_PM10.equals(channelUID.getId()) && statePM10 != UnDefType.UNDEF) { + updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10); + } + } + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + + config = getConfigAs(NovaFineDustConfiguration.class); + + if (!validateConfiguration()) { + return; + } + + // parse ports and if the port is found, initialize the reader + SerialPortIdentifier portId = serialPortManager.getIdentifier(config.port); + if (portId == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port is not known!"); + return; + } + + this.communicator = new SDS011Communicator(this, portId); + + if (config.reporting) { + timeBetweenDataShouldArrive = Duration.ofMinutes(config.reportingInterval); + scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive)); + } else { + timeBetweenDataShouldArrive = Duration.ofSeconds(config.pollingInterval); + scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive)); + } + + Duration connectionMonitorStartDelay = timeBetweenDataShouldArrive.plus(CONNECTION_MONITOR_START_DELAY_OFFSET); + connectionMonitor = scheduler.scheduleWithFixedDelay(this::verifyIfStillConnected, + connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS); + } + + private void initializeCommunicator(WorkMode mode, Duration interval) { + SDS011Communicator localCommunicator = communicator; + if (localCommunicator == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Could not create communicator instance"); + return; + } + + boolean initSuccessful = false; + try { + initSuccessful = localCommunicator.initialize(mode, interval); + } catch (final IOException ex) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!"); + return; + } catch (PortInUseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!"); + return; + } catch (TooManyListenersException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot attach listener to port!"); + return; + } catch (UnsupportedCommOperationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot set serial port parameters"); + return; + } + + if (initSuccessful) { + lastCommunication = ZonedDateTime.now(); + updateStatus(ThingStatus.ONLINE); + + if (mode == WorkMode.POLLING) { + pollingJob = scheduler.scheduleWithFixedDelay(() -> { + try { + localCommunicator.requestSensorData(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Cannot query data from device"); + } + }, 2, config.pollingInterval, TimeUnit.SECONDS); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Commands and replies from the device don't seem to match"); + logger.debug("Could not configure sensor -> setting Thing to OFFLINE and disposing the handler"); + dispose(); + } + } + + private boolean validateConfiguration() { + if (config.port.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!"); + return false; + } + if (config.reporting) { + if (config.reportingInterval < 0 || config.reportingInterval > 30) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Reporting interval has to be between 0 and 30 minutes"); + return false; + } + } else { + if (config.pollingInterval < 3 || config.pollingInterval > 3600) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Polling interval has to be between 3 and 3600 seconds"); + return false; + } + } + return true; + } + + @Override + public void dispose() { + ScheduledFuture localPollingJob = this.pollingJob; + if (localPollingJob != null) { + localPollingJob.cancel(true); + this.pollingJob = null; + } + + ScheduledFuture localConnectionMonitor = this.connectionMonitor; + if (localConnectionMonitor != null) { + localConnectionMonitor.cancel(true); + this.connectionMonitor = null; + } + + SDS011Communicator localCommunicator = this.communicator; + if (localCommunicator != null) { + localCommunicator.dispose(); + } + + this.statePM10 = UnDefType.UNDEF; + this.statePM25 = UnDefType.UNDEF; + } + + /** + * Pass the data from the device to the Thing channels + * + * @param sensorData the parsed data from the sensor + */ + public void updateChannels(SensorMeasuredDataReply sensorData) { + if (sensorData.isValidData()) { + logger.debug("Updating channels with data: {}", sensorData); + + QuantityType statePM10 = new QuantityType<>(sensorData.getPm10(), + SmartHomeUnits.MICROGRAM_PER_CUBICMETRE); + updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10); + this.statePM10 = statePM10; + + QuantityType statePM25 = new QuantityType<>(sensorData.getPm25(), + SmartHomeUnits.MICROGRAM_PER_CUBICMETRE); + updateState(NovaFineDustBindingConstants.CHANNEL_PM25, statePM25); + this.statePM25 = statePM25; + + updateStatus(ThingStatus.ONLINE); + } + // there was a communication, even if the data was not valid, thus resetting the value here + lastCommunication = ZonedDateTime.now(); + } + + private void verifyIfStillConnected() { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTolerance); + if (now.isAfter(lastData)) { + logger.debug("Check Alive timer: Timeout: lastCommunication={}, interval={}, tollerance={}", + lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Check connection cable and afterwards disable and enable this thing to make it work again"); + // in case someone has pulled the plug, we dispose ourselves and the user has to deactivate/activate the + // thing once the cable is plugged in again + dispose(); + } else { + logger.trace("Check Alive timer: All OK: lastCommunication={}, interval={}, tollerance={}", + lastCommunication, timeBetweenDataShouldArrive, dataCanBeLateTolerance); + } + } + + /** + * Set the firmware property on the Thing + * + * @param firmwareVersion the firmware version as a String + */ + public void setFirmware(String firmwareVersion) { + updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java new file mode 100644 index 0000000000000..c86af3d705bc3 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/Command.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class holding the command constants to be send to the sensor in the first data byte + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class Command { + + private Command() { + } + + public static final byte MODE = 2; + public static final byte REQUEST_DATA = 4; + public static final byte HARDWARE_ID = 5; + public static final byte SLEEP = 6; + public static final byte FIRMWARE = 7; + public static final byte WORKING_PERIOD = 8; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java new file mode 100644 index 0000000000000..44132bea4411c --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/ReplyFactory.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; + +/** + * Factory for creating the specific reply instances for data received from the sensor + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class ReplyFactory { + + private static final byte COMMAND_REPLY = (byte) 0xC5; + private static final byte DATA_REPLY = (byte) 0xC0; + + private ReplyFactory() { + } + + /** + * Creates the specific reply message according to the commandID and first data byte + * + * @param bytes the received message + * @return a specific instance of a sensor reply message + */ + public static @Nullable SensorReply create(byte[] bytes) { + if (bytes.length != 10) { + return null; + } + + byte commandID = bytes[1]; + byte firstDataByte = bytes[2]; + + if (commandID == COMMAND_REPLY) { + switch (firstDataByte) { + case Command.FIRMWARE: + return new SensorFirmwareReply(bytes); + case Command.WORKING_PERIOD: + return new WorkingPeriodReply(bytes); + case Command.MODE: + return new ModeReply(bytes); + case Command.SLEEP: + return new SleepReply(bytes); + default: + return new SensorReply(bytes); + } + } else if (commandID == DATA_REPLY) { + return new SensorMeasuredDataReply(bytes); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java new file mode 100644 index 0000000000000..9e3fd9b960cb5 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/SDS011Communicator.java @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Duration; +import java.util.Arrays; +import java.util.TooManyListenersException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.util.HexUtils; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPort; +import org.eclipse.smarthome.io.transport.serial.SerialPortEvent; +import org.eclipse.smarthome.io.transport.serial.SerialPortEventListener; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +import org.openhab.binding.novafinedust.internal.SDS011Handler; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply; +import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Central instance to communicate with the device, i.e. receive data from it and send commands to it + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SDS011Communicator implements SerialPortEventListener { + + private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class); + + private SerialPortIdentifier portId; + private SDS011Handler thingHandler; + private @Nullable SerialPort serialPort; + + private @Nullable OutputStream outputStream; + private @Nullable InputStream inputStream; + + public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId) { + this.thingHandler = thingHandler; + this.portId = portId; + } + + /** + * Initialize the communication with the device, i.e. open the serial port etc. + * + * @param mode the {@link WorkMode} if we want to use polling or reporting + * @param interval the time between polling or reportings + * @return {@code true} if we can communicate with the device + * @throws PortInUseException + * @throws TooManyListenersException + * @throws IOException + * @throws UnsupportedCommOperationException + */ + public boolean initialize(WorkMode mode, Duration interval) + throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException { + boolean initSuccessful = true; + + SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000); + localSerialPort.setSerialPortParams(9600, 8, 1, 0); + + outputStream = localSerialPort.getOutputStream(); + inputStream = localSerialPort.getInputStream(); + + if (inputStream == null || outputStream == null) { + throw new IOException("Could not create input or outputstream for the port"); + } + + // wake up the device + initSuccessful &= sendSleep(false); + initSuccessful &= getFirmware(); + + if (mode == WorkMode.POLLING) { + initSuccessful &= setMode(WorkMode.POLLING); + initSuccessful &= setWorkingPeriod((byte) 0); + } else { + // reporting + initSuccessful &= setWorkingPeriod((byte) interval.toMinutes()); + initSuccessful &= setMode(WorkMode.REPORTING); + } + + // enable listeners only after we have configured the sensor above because for configuring we send and read data + // sequentially + localSerialPort.notifyOnDataAvailable(true); + localSerialPort.addEventListener(this); + this.serialPort = localSerialPort; + + return initSuccessful; + } + + private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException { + byte[] commandData = message.getBytes(); + if (logger.isDebugEnabled()) { + logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData)); + } + + write(commandData); + + try { + // Give the sensor some time to handle the command + Thread.sleep(500); + } catch (InterruptedException e) { + logger.warn("Problem while waiting for reading a reply to our command."); + Thread.currentThread().interrupt(); + } + SensorReply reply = readReply(); + // in case there is still another reporting active, we want to discard the sensor data and read the reply to our + // command again + if (reply instanceof SensorMeasuredDataReply) { + reply = readReply(); + } + return reply; + } + + private void write(byte[] commandData) throws IOException { + OutputStream localOutputStream = outputStream; + if (localOutputStream != null) { + localOutputStream.write(commandData); + localOutputStream.flush(); + } + } + + private boolean setWorkingPeriod(byte period) throws IOException { + CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period }); + logger.debug("Sending work period: {}", period); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to setWorkingPeriod command: {}", reply); + if (reply instanceof WorkingPeriodReply) { + WorkingPeriodReply wpReply = (WorkingPeriodReply) reply; + if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) { + return true; + } + } + return false; + } + + private boolean setMode(WorkMode workMode) throws IOException { + byte haveToRequestData = 0; + if (workMode == WorkMode.POLLING) { + haveToRequestData = 1; + } + + CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData }); + logger.debug("Sending mode: {}", workMode); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to setMode command: {}", reply); + if (reply instanceof ModeReply) { + ModeReply mr = (ModeReply) reply; + if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) { + return true; + } + } + return false; + } + + private boolean sendSleep(boolean doSleep) throws IOException { + byte payload = (byte) 1; + if (doSleep) { + payload = (byte) 0; + } + + CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload }); + logger.debug("Sending doSleep: {}", doSleep); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to sendSleep command: {}", reply); + + if (!doSleep) { + // sometimes the sensor does not wakeup on the first attempt, thus we try again + for (int i = 0; reply == null && i < 3; i++) { + reply = sendCommand(m); + logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply); + } + } + + if (reply instanceof SleepReply) { + SleepReply sr = (SleepReply) reply; + if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) { + return true; + } + } + return false; + } + + private boolean getFirmware() throws IOException { + CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {}); + logger.debug("Sending get firmware request"); + SensorReply reply = sendCommand(m); + logger.debug("Got reply to getFirmware command: {}", reply); + + if (reply instanceof SensorFirmwareReply) { + SensorFirmwareReply fwReply = (SensorFirmwareReply) reply; + thingHandler.setFirmware(fwReply.getFirmware()); + return true; + } + return false; + } + + /** + * Request data from the device, they will be returned via the serialEvent callback + * + * @throws IOException + */ + public void requestSensorData() throws IOException { + CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {}); + byte[] data = m.getBytes(); + if (logger.isDebugEnabled()) { + logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data)); + } + write(data); + } + + private @Nullable SensorReply readReply() throws IOException { + byte[] readBuffer = new byte[Constants.REPLY_LENGTH]; + + InputStream localInpuStream = inputStream; + + int b = -1; + if (localInpuStream != null && localInpuStream.available() > 0) { + while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) { + logger.debug("Trying to find first reply byte now..."); + } + readBuffer[0] = (byte) b; + int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1); + if (logger.isDebugEnabled()) { + logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead, + HexUtils.bytesToHex(readBuffer)); + } + return ReplyFactory.create(readBuffer); + } + return null; + } + + /** + * Data from the device is arriving and will be parsed accordingly + */ + @Override + public void serialEvent(SerialPortEvent event) { + if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) { + // we get here if data has been received + SensorReply reply = null; + try { + reply = readReply(); + logger.debug("Got data from sensor: {}", reply); + } catch (IOException e) { + logger.warn("Could not read available data from the serial port: {}", e.getMessage()); + } + if (reply instanceof SensorMeasuredDataReply) { + SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply; + if (sensorData.isValidData()) { + thingHandler.updateChannels(sensorData); + } + } + } + } + + /** + * Shutdown the communication, i.e. send the device to sleep and close the serial port + */ + public void dispose() { + SerialPort localSerialPort = serialPort; + if (localSerialPort != null) { + try { + // send the device to sleep to preserve power and extend the lifetime of the sensor + sendSleep(true); + } catch (IOException e) { + // ignore because we are shutting down anyway + logger.debug("Exception while disposing communicator (will ignore it)", e); + } finally { + localSerialPort.removeEventListener(); + localSerialPort.close(); + serialPort = null; + } + } + + try { + InputStream localInputStream = inputStream; + if (localInputStream != null) { + localInputStream.close(); + } + } catch (IOException e) { + logger.debug("Error while closing the input stream: {}", e.getMessage()); + } + + try { + OutputStream localOutputStream = outputStream; + if (localOutputStream != null) { + localOutputStream.close(); + } + } catch (IOException e) { + logger.debug("Error while closing the output stream: {}", e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java new file mode 100644 index 0000000000000..2b4eaa30a318a --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/WorkMode.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Enum for the different sensor modes + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public enum WorkMode { + REPORTING, + POLLING +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java new file mode 100644 index 0000000000000..d0e0caff34506 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/CommandMessage.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import java.io.ByteArrayOutputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Message to be send to the device + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class CommandMessage { + private static final byte HEAD = -86; // AA + private static final byte COMMAND_ID = -76; // B4 + private static final byte TAIL = -85; // AB + + private static final int DATA_BYTES_AFTER_FIRST_DATA_BYTE = 12; + + private final byte firstDataByte; + private byte[] payLoad = new byte[DATA_BYTES_AFTER_FIRST_DATA_BYTE]; + private byte[] targetDevice = new byte[] { -1, -1 }; // FF FF = all devices + + public CommandMessage(byte command, byte[] payLoad) { + this.firstDataByte = command; + this.payLoad = payLoad; + } + + public CommandMessage(byte command, byte[] payLoad, byte[] targetDevice) { + this.firstDataByte = command; + this.payLoad = payLoad; + this.targetDevice = targetDevice; + } + + /** + * Get the raw bytes to be send out to the device + * + * @return ByteArray containing the bytes for a message to the device + */ + public byte[] getBytes() { + ByteArrayOutputStream message = new ByteArrayOutputStream(19); + + message.write(HEAD); + message.write(COMMAND_ID); + message.write(firstDataByte); + + for (byte b : payLoad) { + message.write(b); + } + int padding = DATA_BYTES_AFTER_FIRST_DATA_BYTE - payLoad.length; + for (int i = 0; i < padding; i++) { + message.write(0x00); + } + + for (byte b : targetDevice) { + message.write(b); + } + message.write(calculateCheckSum(message.toByteArray())); + message.write(TAIL); + + return message.toByteArray(); + } + + private byte calculateCheckSum(byte[] data) { + int checksum = 0; + for (int i = 2; i <= 14; i++) { + checksum += data[i]; + } + checksum = (checksum - 2) % 256; + + return (byte) checksum; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Message: "); + sb.append("Command=" + firstDataByte); + sb.append(" Target Device=" + HexUtils.bytesToHex(targetDevice)); + sb.append(" Payload=" + HexUtils.bytesToHex(payLoad)); + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java new file mode 100644 index 0000000000000..8ed17a7ec04ed --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/Constants.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants for sensor messages + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class Constants { + + private Constants() { + } + + public static final byte MESSAGE_START = (byte) 0xAA; + public static final int MESSAGE_START_AS_INT = 170; + public static final byte MESSAGE_END = (byte) 0xAB; + + public static final int REPLY_LENGTH = 10; + + public static final byte QUERY_ACTION = (byte) 0x00; + public static final byte SET_ACTION = (byte) 0x01; +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java new file mode 100644 index 0000000000000..9c52d14807498 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/ModeReply.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.novafinedust.internal.sds011protocol.WorkMode; + +/** + * Reply from sensor to a set mode command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class ModeReply extends SensorReply { + + private final byte actionType; + private final WorkMode mode; + + public ModeReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + if (bytes[4] == (byte) 1) { + this.mode = WorkMode.POLLING; + } else { + this.mode = WorkMode.REPORTING; + } + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the set work mode + * + * @return work mode set on the sensor + */ + public WorkMode getMode() { + return mode; + } + + @Override + public String toString() { + return "ModeReply: [mode=" + mode + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java new file mode 100644 index 0000000000000..3e15c541c9030 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorFirmwareReply.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Data from the sensor containing information about the installed firmware + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorFirmwareReply extends SensorReply { + + private final byte year; + private final byte month; + private final byte day; + + public SensorFirmwareReply(byte[] receivedData) { + super(receivedData); + this.year = receivedData[3]; + this.month = receivedData[4]; + this.day = receivedData[5]; + } + + /** + * Gets the firmware of the sensor as a String + * + * @return firmware of the sensor formatted as YY-MM-DD + */ + public String getFirmware() { + String firmware = year + "-" + month + "-" + day; + return firmware; + } + + @Override + public String toString() { + return "FirmwareReply: [firmware=" + getFirmware() + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java new file mode 100644 index 0000000000000..05ee374e4c4eb --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorMeasuredDataReply.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Class containing the actual measured values from the sensor + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorMeasuredDataReply extends SensorReply { + private final byte pm25lowByte; + private final byte pm25highByte; + private final byte pm10lowByte; + private final byte pm10highByte; + + /** + * Create a new instance by parsing the given 10 bytes. + * + */ + public SensorMeasuredDataReply(byte[] bytes) { + super(bytes); + pm25lowByte = bytes[2]; + pm25highByte = bytes[3]; + pm10lowByte = bytes[4]; + pm10highByte = bytes[5]; + } + + /** + * Check if data is valid by checking header, commanderNo, messageTail and checksum. + */ + public boolean isValidData() { + return header == Constants.MESSAGE_START && commandID == (byte) 0xC0 && messageTail == Constants.MESSAGE_END + && checksum == calculateChecksum(); + } + + /** + * Get the measured PM2.5 value + * + * @return the measured PM2.5 value + */ + public float getPm25() { + int shiftedValue = (pm25highByte << 8 & 0xFF) | pm25lowByte & 0xFF; + return ((float) shiftedValue) / 10; + } + + /** + * Get the measured PM10 value + * + * @return the measured PM10 value + */ + public float getPm10() { + int shiftedValue = (pm10highByte << 8 & 0xFF) | pm10lowByte & 0xFF; + return ((float) shiftedValue) / 10; + } + + @Override + public String toString() { + return String.format( + "SensorMeasuredDataReply: [valid=%s, PM 2.5=%.1f, PM 10=%.1f, sourceDevice=%s, pm25lowHigh=(%s) pm10lowHigh=(%s)]", + isValidData(), getPm25(), getPm10(), HexUtils.bytesToHex(new byte[] { deviceID[0], deviceID[1] }), + HexUtils.bytesToHex(new byte[] { pm25lowByte, pm25highByte }), + HexUtils.bytesToHex(new byte[] { pm10lowByte, pm10highByte })); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java new file mode 100644 index 0000000000000..f2f0d23e916e8 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SensorReply.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.util.HexUtils; + +/** + * Base class holding information sent by the sensor to us + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SensorReply { + + protected final byte header; + protected final byte commandID; + protected final byte[] payLoad; + protected final byte[] deviceID; + protected final byte checksum; + protected final byte messageTail; + + /** + * Creates the container for data received from the sensor + * + * @param bytes the data received from the sensor + * @throws IllegalArgumentException Is thrown if less than 10 bytes are provided. + */ + public SensorReply(byte[] bytes) { + if (bytes.length != 10) { + throw new IllegalArgumentException("was expecting 10 bytes, but received " + bytes.length); + } + this.header = bytes[0]; + this.commandID = bytes[1]; + this.payLoad = Arrays.copyOfRange(bytes, 2, 6); + this.deviceID = Arrays.copyOfRange(bytes, 6, 8); + this.checksum = bytes[8]; + this.messageTail = bytes[9]; + } + + /** + * Gets the commandID byte. However there is the first data byte which holds a kind of "sub command" that has to be + * evaluated too + * + * @return byte representing the commandID + */ + public byte getCommandID() { + return this.commandID; + } + + /** + * Gets the first byte from the data bytes (usually holds the {@link Command}) as a form of some sub command + * + * @return first byte from the data section of a reply + */ + public byte getFirstDataByte() { + return this.payLoad[0]; + } + + protected byte calculateChecksum() { + byte sum = 0; + for (byte b : payLoad) { + sum += b; + } + for (byte b : deviceID) { + sum += b; + } + return sum; + } + + @Override + public String toString() { + return String.format("GeneralReply: [head=%x, commandID=%x, payload=%s, deviceID=%s, checksum=%s, tail=%x", + header, commandID, HexUtils.bytesToHex(payLoad), HexUtils.bytesToHex(deviceID), checksum, messageTail); + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java new file mode 100644 index 0000000000000..19b8d250c7ba7 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/SleepReply.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Reply from sensor to a set sleep command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class SleepReply extends SensorReply { + + private final byte actionType; + private final byte sleep; + + public SleepReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + this.sleep = bytes[4]; + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the info whether this is a sleep or wakeup reply + * + * @return 0 = sleep 1 = work + */ + public byte getSleep() { + return sleep; + } + + @Override + public String toString() { + return "SleepReply: [sleep=" + sleep + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java new file mode 100644 index 0000000000000..b882243edfbff --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/java/org/openhab/binding/novafinedust/internal/sds011protocol/messages/WorkingPeriodReply.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.novafinedust.internal.sds011protocol.messages; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Reply from sensor to a set working period command + * + * @author Stefan Triller - Initial contribution + * + */ +@NonNullByDefault +public class WorkingPeriodReply extends SensorReply { + + private final byte actionType; + private final byte period; + + public WorkingPeriodReply(byte[] bytes) { + super(bytes); + + this.actionType = bytes[3]; + this.period = bytes[4]; + } + + /** + * Get the type of action + * + * @return 0 = query 1 = set mode + */ + public byte getActionType() { + return actionType; + } + + /** + * Get the set working period + * + * @return working period set on the sensor + */ + public byte getPeriod() { + return period; + } + + @Override + public String toString() { + return "WorkingPeriodReply: [Period=" + this.period + "]"; + } +} diff --git a/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..2761c8a57e4e1 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + NovaFineDust Binding + This is the binding for Nova Fitness Fine Dust SDS011 sensor. + Stefan Triller + + diff --git a/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..b44f880cf0483 --- /dev/null +++ b/bundles/org.openhab.binding.novafinedust/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,61 @@ + + + + + + Nova SDS011 Fine Dust Sensor connected via USB + + + + + + + + + serial-port + + USB port the device is connected to i.e. /dev/ttyUSB0 + + + true + + + + + + Reporting is strongly recommended to increase sensor lifetime + + + 1 + true + + Device will report every x minutes and sleep for x*60 - 30 seconds afterwards, 0 = as fast as possible without sleep + + + 10 + true + + Device will be polled every x seconds (polling is not recommended) + + + + + + + Number:Density + + The PM 2.5 value + + + + + Number:Density + + The PM 10 value + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index ba9fa022c05ae..e8f33bbe9b760 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -172,6 +172,7 @@ org.openhab.binding.nibeuplink org.openhab.binding.nikobus org.openhab.binding.nikohomecontrol + org.openhab.binding.novafinedust org.openhab.binding.ntp org.openhab.binding.nuki org.openhab.binding.oceanic