added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user