added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.novafinedust-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-novafinedust" description="NovaFineDust Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.novafinedust/${project.version}</bundle>
</feature>
</features>

View File

@@ -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.openhab.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";
}

View File

@@ -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;
}

View File

@@ -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.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
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<ThingTypeUID> 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;
}
}

View File

@@ -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.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.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.openhab.core.library.dimension.Density;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
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<Density> statePM10 = new QuantityType<>(sensorData.getPm10(),
SmartHomeUnits.MICROGRAM_PER_CUBICMETRE);
updateState(NovaFineDustBindingConstants.CHANNEL_PM10, statePM10);
this.statePM10 = statePM10;
QuantityType<Density> 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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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.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.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.openhab.core.util.HexUtils;
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());
}
}
}

View File

@@ -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
}

View File

@@ -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.openhab.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();
}
}

View File

@@ -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;
}

View File

@@ -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 + "]";
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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.openhab.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 }));
}
}

View File

@@ -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.openhab.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);
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="novafinedust" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>NovaFineDust Binding</name>
<description>This is the binding for Nova Fitness Fine Dust SDS011 sensor.</description>
<author>Stefan Triller</author>
</binding:binding>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="novafinedust"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="SDS011">
<label>Nova SDS011 Fine Dust Sensor</label>
<description>Nova SDS011 Fine Dust Sensor connected via USB</description>
<channels>
<channel id="pm10" typeId="pm10-type"/>
<channel id="pm25" typeId="pm25-type"/>
</channels>
<config-description>
<parameter name="port" type="text" required="true">
<context>serial-port</context>
<label>USB Port</label>
<description>USB port the device is connected to i.e. /dev/ttyUSB0</description>
</parameter>
<parameter name="reporting" type="boolean">
<default>true</default>
<label>Mode</label>
<options>
<option value="true">Reporting</option>
<option value="false">Polling</option>
</options>
<description>Reporting is strongly recommended to increase sensor lifetime</description>
</parameter>
<parameter name="reportingInterval" type="integer" min="0" max="30" unit="min">
<default>1</default>
<advanced>true</advanced>
<label>Reporting Interval</label>
<description>Device will report every x minutes and sleep for x*60 - 30 seconds afterwards, 0 = as fast as possible
without sleep</description>
</parameter>
<parameter name="pollingInterval" type="integer" min="3" max="3600" unit="s">
<default>10</default>
<advanced>true</advanced>
<label>Polling Interval</label>
<description>Device will be polled every x seconds (polling is not recommended)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="pm25-type">
<item-type>Number:Density</item-type>
<label>PM 2.5</label>
<description>The PM 2.5 value</description>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
<channel-type id="pm10-type">
<item-type>Number:Density</item-type>
<label>PM 10</label>
<description>The PM 10 value</description>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
</thing:thing-descriptions>