Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[novafinedust] Nova Fine Dust binding for SDS011 sensors #7528

Merged
merged 13 commits into from Jun 14, 2020

Conversation

t2000
Copy link
Contributor

@t2000 t2000 commented May 2, 2020

Closes #7527

Signed-off-by: Stefan Triller github@stefantriller.de

@t2000 t2000 requested a review from a team as a code owner May 2, 2020 15:21
@t2000
Copy link
Contributor Author

t2000 commented May 2, 2020

I have the binding attached for testing, please rename to ".jar" before putting it into the addons folder.

org.openhab.binding.novafinedust-2.5.5-SNAPSHOT.zip

@TravisBuddy
Copy link

Travis tests have failed

Hey @t2000,
please read the following log in order to understand the failure reason. There might also be some helpful tips along the way.
It will be awesome if you fix what is wrong and commit the changes.

@t2000 t2000 force-pushed the novafinedust branch 2 times, most recently from 082d9ea to 17ab9e4 Compare May 2, 2020 15:42
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

@cpmeister cpmeister added the new binding If someone has started to work on a binding. For a new binding PR. label May 3, 2020

| parameter name | mandatory | description |
|-------------------|-----------|---------------------------------------------------------------------------------------|
| port | yes | the port the sensor is connected to, i.e. /detv/ttyUSB0. |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| port | yes | the port the sensor is connected to, i.e. /detv/ttyUSB0. |
| port | yes | the port the sensor is connected to, i.e. /dev/ttyUSB0. |


// initialize timeBetweenDataShouldArrive with a large number
private Duration timeBetweenDataShouldArrive = Duration.ofDays(1);
private final Duration dataCanBeLateTollerance = Duration.ofSeconds(5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo "Tolerance"

try {
communicator.requestSensorData();
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Thing is not set as online again, if the connection succeeds in one of the upcoming cycles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be set to ONLINE again once there is valid data received, see method updateChannels.

Comment on lines 157 to 161
private boolean validateConfiguration() {
if (config.port == null || 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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to check the config again, if you configured min/max etc. in the XML files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, so openHAB now validates these values before passing them to the handler? That's cool, I didn't know that this was implemented. Thanks for letting me know. I have removed the checks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I have to retract my words. The config is only validated in Paper UI, but not when configuring via a things file. @cpmeister should bindings double check their config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is always better if the binding double checks the configs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I have reverted the removal of the check, so it is back in place.

private void verifyIfStillConnected() {
ZonedDateTime now = ZonedDateTime.now();
Temporal lastData = lastCommunication.plus(timeBetweenDataShouldArrive).plus(dataCanBeLateTollerance);
if (now.isAfter((ChronoZonedDateTime<?>) lastData)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use ZonedDateTime instead of Temporal, you could remove this cast.

reply = readReply();
logger.debug("Got data from sensor: {}", reply);
} catch (IOException e) {
logger.error("Could not read available data from the serial port", e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bindings should only log as error, when a bug in the code is detected. You could use warn here.

Comment on lines 284 to 285
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are going to remove the Apache Commons dependencies, could you replace the lines with standard Java code? See #7722

t2000 added 2 commits May 23, 2020 14:47
Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
Signed-off-by: Stefan Triller <github@stefantriller.de>
@t2000
Copy link
Contributor Author

t2000 commented May 23, 2020

Thanks for the review. I have addressed your comments, except for one where I replied here on github.

I have also rebased the code against the latest 2.5.x branch.

@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

Copy link
Member

@fwolter fwolter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, except the single log message.

@cpmeister The dev was very responsive. Can you take a look?

@@ -253,7 +252,7 @@ public void serialEvent(SerialPortEvent event) {
reply = readReply();
logger.debug("Got data from sensor: {}", reply);
} catch (IOException e) {
logger.error("Could not read available data from the serial port", e);
logger.warn("Could not read available data from the serial port", e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to log the stacktrace here, but you could log the exception message:

Suggested change
logger.warn("Could not read available data from the serial port", e);
logger.warn("Could not read available data from the serial port: {}", e.getMessage());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this makes sense to only log the message. I have changed it, thanks!

Signed-off-by: Stefan Triller <github@stefantriller.de>
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

@cpmeister cpmeister changed the title Nova Fine Dust binding for SDS011 sensors [novafinedust] Nova Fine Dust binding for SDS011 sensors May 23, 2020
* @param bytes the byte array to be converted
* @return a String describing the byte array in hexadecimal values
*/
public static String toHexString(byte[] bytes) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an implementation of this in HexUtils from openhab core.

Comment on lines 75 to 76
// we do not support refreshing as values are either reported by the device or polled from the device in fixed
// intervals
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you could cache the values so that a Refresh command would be able to get the most recently reported ones.

}

if (communicator != null) {
scheduler.schedule(() -> communicator.dispose(), 0, TimeUnit.SECONDS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this disposed asynchronously?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I think initialize and dispose from a ThingHandler should return quickly, i.e. within 5 seconds. Inside the dispose I send the sensor to sleep by communicating with it. This is important to enhance its lifetime.

So in order to not block the disposing call, I do it asynchronously.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens if the handler is restarted (disposed then initialized again)? Would the handler be stuck in a configuration COMMUNICATION_ERROR because the port is still in use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a VERY interesting point! :-)

There are similar problems in the zigbee binding, because there it also takes a while to startup the stack and if this gets restarted through dispose/initialize calls, everything gets messed up too. Synchronization might help there but is a pain in the ass to do that for every binding.

And I am afraid to say that with the 5 second rule for initialize/dispose, bindings are forced to spawn a separate task, which will ALWAYS cause these troubles if dispose/initialize is called within a short time frame (i.e. by a disable/enable of the thing).

If you prefer to have my dispose here running in sync with the framework, I am fine to replace the task with a direct call. In my case this usually only takes about 1-2 seconds anyway, but I wanted to play safe since I am communicating with an external device and this might timeout, etc.

But the overall problem for all bindings will stay, especially with the use case of enabling and disabling of a thing.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way I can see of dealing with delayed initialize/dispose is to make your handler and communicator objects have different lifecycles.

You would need to create a your own communicator factory class that handlers would request communicators for a given port.

public interface CommunicatorFactory{
     SDS011Communicator getCommunicator(SerialPortIdentifier portId);
}

The factory itself would share the same lifecycle as an osgi component so either you could make your handler factory a communicator factory or implement it in a new osgi component.

The key benefit here is that you would be able to reuse SDS011Communicator instances across handler restarts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really solve the problem, I could still have the communicator instance running if i dispose the handler and then the port is blocked.

Also its not worth the effort as I said before the communication takes max 1-2 seconds.

I have changed it to be synchronous now.

Copy link
Contributor

@cpmeister cpmeister May 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really solve the problem, I could still have the communicator instance running if i dispose the handler and then the port is blocked.

Not really since you can prevent a communicator from opening the port if it is already open. Likewise a communicator can stop its disposal process if a new handler requests to use it.

I have changed it to be synchronous now.

I don't think would work, a new handler means a new communicator object. Synchronization only works on the same object instance.
Nvm, just saw what you committed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really since you can prevent a communicator from opening the port if it is already open. Likewise a communicator can stop its disposal process if a new handler requests to use it.

The problem is not having a different thing, let's say "thing2" wanting the very same port. This is already handled, because the port in use exception is dealt with. And this is always a user problem, since he has configured thing1 and thing2 with the same port.

What I am talking about is that the ThingHandler from thing1 is disposed and shortly afterwards initialized again. This can happen if one disabled and enables a thing, see https://github.com/openhab/openhab-core/blob/5b325aa3d210c2dfa490c676520fe755b0f3fe1c/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java#L1167-L1216

Here it doesnt matter if a communicator object or the thinghandler itself want to open the port on initialize. If it is currently in use by a thinghandler that gets disposed at the moment, then there is no way for a starting thinghandler to open the port and he will run into the port in use exception.

This applies to ALL binding which are operating on a shared resource like a port btw...

So with the change I did yesterday evening, the dispose is always called and finished before an initialize, so for now all is good within this binding.

However, one should think about the general problem of shared resources and scenarios where an initialize or dispose might take longer and one has no other choice than putting this in an external task because of this 5 second rule from openHAB.


private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_SDS011);

private @NonNullByDefault({}) SerialPortManager serialPortManager;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest populating this through the NovaFineDustHandlerFactory constructor.

Suggested change
private @NonNullByDefault({}) SerialPortManager serialPortManager;
private final SerialPortManager serialPortManager;
@Activate
public NovaFineDustHandlerFactory(@Reference SerialPortManager serialPortManager){
this.serialPortManager = serialPortManager;
}

Then you can remove the setSerialPortManager and unsetSerialPortManager methods.


@Override
public void dispose() {
if (pollingJob != null && !pollingJob.isCancelled()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No harm in cancelling something that is already cancelled.

Suggested change
if (pollingJob != null && !pollingJob.isCancelled()) {
if (pollingJob != null) {

* @author Stefan Triller - Initial contribution
*
*/
public enum WorkMode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add @NonNullByDefault

Signed-off-by: Stefan Triller <github@stefantriller.de>
@t2000
Copy link
Contributor Author

t2000 commented May 25, 2020

@cpmeister Thanks for your review. Again I learned something about OSGi (reference thingy in the constructor) and that openHAB has some units on the thing-types.xmls :)

I have addressed all your comments and replied to your questions. Please have a look.

@TravisBuddy
Copy link

Travis tests have failed

Hey @t2000,
please read the following log in order to understand the failure reason. There might also be some helpful tips along the way.
It will be awesome if you fix what is wrong and commit the changes.

Signed-off-by: Stefan Triller <github@stefantriller.de>
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

Signed-off-by: Stefan Triller <github@stefantriller.de>
@TravisBuddy
Copy link

Hey @t2000,
Something went wrong with the build.

TravisCI finished with status errored, which means the build failed because of something unrelated to the tests, such as a problem with a dependency or the build process itself.

View build log

TravisBuddy Request Identifier: aecad270-9f71-11ea-a34f-dd9ab62511aa

if (serialPort != null) {
try {
// send the device to sleep to preserve power and extend the lifetime of the sensor
sendSleep(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you remove the event listener before you call this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this before, but somehow the underlieing library does something with the port if I de-register my listener and then my command doesn't go through.

This is just a guess from my observation. That is why I did not de-register the listener before that call here.

Also in the callback method for the listener I check for valid data, so it doesn't matter if the callback receives the reply to the sleep on shutdown.

Signed-off-by: Stefan Triller <github@stefantriller.de>
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

@t2000
Copy link
Contributor Author

t2000 commented May 31, 2020

Thanks for the review. I have addressed your changes.

@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

Comment on lines 114 to 115
scheduler.schedule(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive), 0,
TimeUnit.SECONDS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
scheduler.schedule(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive), 0,
TimeUnit.SECONDS);
scheduler.submit(() -> initializeCommunicator(WorkMode.REPORTING, timeBetweenDataShouldArrive));

Comment on lines 118 to 119
scheduler.schedule(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive), 0,
TimeUnit.SECONDS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
scheduler.schedule(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive), 0,
TimeUnit.SECONDS);
scheduler.submit(() -> initializeCommunicator(WorkMode.POLLING, timeBetweenDataShouldArrive));

connectionMonitorStartDelay.getSeconds(), timeBetweenDataShouldArrive.getSeconds(), TimeUnit.SECONDS);
}

private void initializeCommunicator(WorkMode mode, Duration interval) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be safe, I would make sure that initialize, dispose, and initializeCommunicator are synchronized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should I do that? This won't work if I write synchronized on those methods.

The reason is that this will synchronize of the ´ThingHandlerobject and this is never the same is adisposeand aninitializeare called. TheThingManagerwill always create a newThingHandlerinstance before callinginitialize, so the call to dispose` will happen on a different (old) object.

This is what I wanted to state earlier: The openHAB framework currently does NOT ensure that dispose and initialize run into each other, if one is forced to schedule a task (that runs in a different thread) from initialize if one needs more than 5 seconds.

So either: I do NOT schedule this task and risk the warning "ThingHandler initialize takes longer than 5 seconds", or I take the (in my case pretty unlikely) risk that dispose and initialize collide eventually.

So which option should I pick?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@J-N-K Could you please weigh in here? I can't tell if I'm being overly concerned or not with regards to thread safety here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can chime in here. We never have synchronized keywords on such methods. As @t2000 mentions, the work is done in a separate thread and it is actually intended that the framework is able to dispose the handler, while such jobs might still be running. They should NOT block a call of dispose, so synchronizing it would be counter-productive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaikreuzer I'm just concerned that initializeCommunicator would be called after dispose since initializeCommunicator is called asynchronously.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds pretty unlikely and even if it does, the code should be written in a way that it shouldn't harm much.
If dispose would set the communicator to null and initializeCommunicator would not try to set the Thing status upon a null-communicator, it should almost be safe ;-)

Signed-off-by: Stefan Triller <github@stefantriller.de>
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

@cpmeister cpmeister added the cre Coordinated Review Effort label Jun 2, 2020
@t2000
Copy link
Contributor Author

t2000 commented Jun 6, 2020

Thank you for the reviews and the approvals. is there anything else that needs to be done from my side before merging this? If so, please ping me.

@fwolter
Copy link
Member

fwolter commented Jun 6, 2020

No, you did fine. All you have to do is to wait for another maintainer to approve your PR. That's why the CRE label has been added.

Copy link
Member

@kaikreuzer kaikreuzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, @t2000 - please find some final review comments below.

@kaikreuzer kaikreuzer self-assigned this Jun 13, 2020
@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

Thanks Kai for your suggestions, I have accepted them all and will take care of the few others in an additional commit.

Co-authored-by: Kai Kreuzer <kai@openhab.org>
Signed-off-by: Stefan Triller <github@stefantriller.de>
Signed-off-by: Stefan Triller <github@stefantriller.de>
@t2000
Copy link
Contributor Author

t2000 commented Jun 14, 2020

Thanks, for the final review @kaikreuzer.
I have added 2 more commits, one with the suggestions that you posted on github and one with the clarifications that you requested on the README.

Please have a look if its OK or I missed something. Thank you!

@TravisBuddy
Copy link

Travis tests were successful

Hey @t2000,
we found no major flaws with your code. Still you might want to look at this logfile, as we usually suggest some optional improvements.

@openhab openhab deleted a comment from TravisBuddy Jun 14, 2020
Copy link
Member

@kaikreuzer kaikreuzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick updates, lgtm now!

@kaikreuzer kaikreuzer merged commit 0b20457 into openhab:2.5.x Jun 14, 2020
@t2000 t2000 deleted the novafinedust branch June 14, 2020 12:12
@cpmeister cpmeister added this to the 2.5.6 milestone Jun 14, 2020
knikhilwiz pushed a commit to knikhilwiz/openhab2-addons that referenced this pull request Jul 12, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
CSchlipp pushed a commit to CSchlipp/openhab-addons that referenced this pull request Jul 26, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
Signed-off-by: CSchlipp <christian@schlipp.de>
andrewfg pushed a commit to andrewfg/openhab-addons that referenced this pull request Aug 31, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
andrewfg pushed a commit to andrewfg/openhab-addons that referenced this pull request Aug 31, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
andrewfg pushed a commit to andrewfg/openhab-addons that referenced this pull request Aug 31, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
andrewfg pushed a commit to andrewfg/openhab-addons that referenced this pull request Aug 31, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
DaanMeijer pushed a commit to DaanMeijer/openhab-addons that referenced this pull request Sep 1, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
Signed-off-by: Daan Meijer <daan@studioseptember.nl>
markus7017 pushed a commit to markus7017/openhab-addons that referenced this pull request Sep 19, 2020
* Nova Fine Dust binding for SDS011 sensors

Closes openhab#7527

Signed-off-by: Stefan Triller <github@stefantriller.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cre Coordinated Review Effort new binding If someone has started to work on a binding. For a new binding PR.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[novafinedust] binding for Nova Fitness SDS011 sensors
5 participants