Levoit Binding

Hi,

I have like no doubt like many - brought a Levoit (400S/300S or 200S) Air Purifier in the current times, given the offers that always appear on Amazon (after our other “not named” expensive brand nearly caught fire :frowning: ). On researching integration to OpenHab I didn’t want to spawn third party python processes (given server loads) but instead have a native binding. Paying for certain expensive brand’s I could not qualify to my wife, for integration to our house system. I have therefore used the great work from pyvesync (GitHub - webdjoe/pyvesync: pyvesync is a python library to manage Etekcity smart devices) to create a binding for the Core200S/Core300S and Core400S Levoit based air purifiers, which will hopefully work for the many and bring the filters price down if its adopted by the masses :slight_smile: I can only test with the Core400S unit as that’s what I have, so feedback on the others would be appreciated thanks (especially if the night light functionality works as expected - I can’t test this on the 400S). In the long run the older LV model (and the humidifier) could be adapted to get full coverage (but they appear to use a different API version - so I’m uploading the modules now to get feedback on the 200/300/400 models. Longer term the lib’s should be able to be re-used for other Vesync based devices, although I would imagine given the polling nature - non real time devices such as appliance’s are much more suitable, than switches or things that require very fast feedback for this code base / setup nature). Anyway this is a first push (for feedback), prior to pushing an official fork, to get an official binding added.

The source code is here: GitHub - dag81/vesyncOpenhabPyPort: Port of Air Purifier support to openhab binding.

The landing page above has hopefully enough information to setup the binding. (For at least the air filters at the moment).

Thanks to SurePet authors and Hue binding authors who’s code I looked at to understand the behaviour’s of OpenHab.(Apologies I will correct the names in the comment blocks I copied and pasted to remove the spotless errors later on - but this was a very quick first attempt).

Any feedback appreciated, while I patch at weekends and evenings.

1 Like

FYI I’ve just bought a Levoit 300S Humidifier. I’ll be glad to conduct some test if you wish to

Hi @lyo125,

That would be great thanks. I’ve pushed a build on to the repo located at this location which has been built and I am running on my home system: 3.2.0.M3 - Milestone Build.

I’ve done some updates to the documentation page, which include what I “think” should be the settings for the Core200/300S units : GitHub - dag81/vesyncOpenhabPyPort: Port of Air Purifier support to openhab binding

It would be great to check all the bits in the example sitemap read as expected, especially night light and the air quality. Additionally the setting of the night light. :crossed_fingers: I got the port of the protocol right based on pyvesync.

1 Like

Perfect, I’ll have a look in the following days. I’m running the latest stable release of OH. I’ll try to manually install your binding and let you know if any help is required.

After a Quick Test:

The bridge Item is always Online, even if I try providing it wrong credentials…

I then tried to add the Humidifier 300s using the item type “Air purifier” as this was to only type available. The discovery of the item didn’t work. I’ve manually added the thing and set the bridge to use the right one (by default the bridge configuration was blank). I’ve setup Device Name to be the same as in the application. The thing keep staying at “UNKNOWN” state.

Log trace:

2021-11-06 13:24:49.842 [WARN ] [mmon.WrappedScheduledExecutorService] - Scheduled runnable ended with an exception: 
java.lang.NullPointerException: null
	at org.openhab.binding.vesync.internal.dto.requests.VesyncAuthenticatedRequest.<init>(VesyncAuthenticatedRequest.java:38) ~[?:?]
	at org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage.<init>(VesyncRequestManagedDevicesPage.java:42) ~[?:?]
	at org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper.discoverDevices(VesyncV2ApiHelper.java:101) ~[?:?]
	at org.openhab.binding.vesync.internal.VeSyncBridgeHandler.runDeviceScanSequence(VeSyncBridgeHandler.java:139) ~[?:?]
	at org.openhab.binding.vesync.internal.VeSyncBridgeHandler.lambda$5(VeSyncBridgeHandler.java:188) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
	at java.lang.Thread.run(Thread.java:834) [?:?]

and

2021-11-06 13:20:42.576 [ERROR] [nternal.DiscoveryServiceRegistryImpl] - Cannot trigger scan for thing types '[vesync:bridge]' on 'VeSyncDiscoveryService'!
java.lang.NullPointerException: null
	at org.openhab.binding.vesync.internal.discovery.VeSyncDiscoveryService.startScan(VeSyncDiscoveryService.java:108) ~[?:?]
	at org.openhab.core.config.discovery.AbstractDiscoveryService.startScan(AbstractDiscoveryService.java:194) ~[?:?]
	at org.openhab.core.config.discovery.internal.DiscoveryServiceRegistryImpl.startScan(DiscoveryServiceRegistryImpl.java:377) ~[?:?]
	at org.openhab.core.config.discovery.internal.DiscoveryServiceRegistryImpl.startScans(DiscoveryServiceRegistryImpl.java:353) ~[?:?]
	at org.openhab.core.config.discovery.internal.DiscoveryServiceRegistryImpl.startScan(DiscoveryServiceRegistryImpl.java:211) ~[?:?]
	at org.openhab.core.io.rest.core.internal.discovery.DiscoveryResource.scan(DiscoveryResource.java:105) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]
	at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:179) ~[bundleFile:3.4.3]
	at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[bundleFile:3.4.3]
	at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:201) ~[bundleFile:3.4.3]
	at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:104) ~[bundleFile:3.4.3]
	at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[bundleFile:3.4.3]
	at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[bundleFile:3.4.3]
	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:225) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:298) ~[bundleFile:3.4.3]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:217) ~[bundleFile:3.4.3]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707) ~[bundleFile:3.1.0]
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:273) ~[bundleFile:3.4.3]
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:791) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:550) ~[bundleFile:9.4.40.v20210413]
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceServletHandler.doHandle(HttpServiceServletHandler.java:71) ~[bundleFile:?]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:602) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1435) ~[bundleFile:9.4.40.v20210413]
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceContext.doHandle(HttpServiceContext.java:294) ~[bundleFile:?]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:501) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1350) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[bundleFile:9.4.40.v20210413]
	at org.ops4j.pax.web.service.jetty.internal.JettyServerHandlerCollection.handle(JettyServerHandlerCollection.java:82) ~[bundleFile:?]
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:388) ~[bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:633) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:380) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:383) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:882) [bundleFile:9.4.40.v20210413]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1036) [bundleFile:9.4.40.v20210413]
	at java.lang.Thread.run(Thread.java:834) [?:?]

Hi @lyo125,

Thanks I’ll quickly add a test and patch up that bridge status first as that will affect all air purifier users - I think I know what I missed, during the many refactors. I missed the key part that you said which is that you have the “Humidifer” not 300S Air Filter. I have not mapped that protocol part in yet, I’ll have a look tonight at mapping that elements into the code base, and find out why you get null anyway, as it shouldn’t be doing that. When you got the null pointer were the credentials incorrect out of interest?

I’ve disable both the bridge and the Humidifier and made sure I had the original good credentials. Even with the good credentials I’m getting

2021-11-06 14:25:47.386 [WARN ] [mmon.WrappedScheduledExecutorService] - Scheduled runnable ended with an exception: 
java.lang.NullPointerException: null
	at org.openhab.binding.vesync.internal.dto.requests.VesyncAuthenticatedRequest.<init>(VesyncAuthenticatedRequest.java:38) ~[?:?]
	at org.openhab.binding.vesync.internal.dto.requests.VesyncRequestManagedDevicesPage.<init>(VesyncRequestManagedDevicesPage.java:42) ~[?:?]
	at org.openhab.binding.vesync.internal.api.VesyncV2ApiHelper.discoverDevices(VesyncV2ApiHelper.java:101) ~[?:?]
	at org.openhab.binding.vesync.internal.VeSyncBridgeHandler.runDeviceScanSequence(VeSyncBridgeHandler.java:139) ~[?:?]
	at org.openhab.binding.vesync.internal.VeSyncBridgeHandler.lambda$5(VeSyncBridgeHandler.java:188) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
	at java.lang.Thread.run(Thread.java:834) [?:?]

Hi @lyo125,

Thank you :slight_smile: - I have been looking through, just patching up the authentication lifecycle. Im guessing you were configuring via the UI. After some other updates I’ve just done, I just need to detect the case where the UI is updated configuration wise, then will push another one that should fix the authenication bridge online / offline. Then I’ll go back to adding the humidifier support - I’ll probably get it reading the data first. Then ask if you wouldn’t mind sending a capture of the debug of the data received, so I can adjust as relevant before adding the ability to send commands.

Hi @lyo125,

This release should fix the authentication issues that you had. You will have to re-enter email address / username against the bridge thing. I’ll continue to look at getting the humidifier support in now :slight_smile:
[vesyncOpenhabPyPort/org.openhab.binding.vesync-3.2.0-SNAPSHOT.jar at main · dag81/vesyncOpenhabPyPort · GitHub](https://v0.2.0 Authentication Corrections)

Hi @lyo125, When you have some spare time it would be great if you could try loading this build:

v0.2.1 build with Humidifier read only support (hopefully)

If I have not missed something silly it should add support to read the humidifier’s data. I have not updated the documentation yet, so it will have to be driven by the UI. Unfortunately as I don’t have one I’ve done my best based on the data in pyvesync. :crossed_fingers: It reads the relevant data, and have not missed anything.

What would be good to know assuming there are no hip-cups, is if any of the channels of data have units such as moisture levels. (Its unclear from VeSync if there are SI or other units relevant to the data).

:crossed_fingers: You can discover and read the data from the unit now

Please let me know when you do have some spare time thanks.

Thanks again

David

Hi @lyo125,

I’ve pushed a new build of the jar file, and updated the documentation in the Readme. I still need to take a guess at what the site-map should be for the humidifier. This should remove the exception you saw and replace it with a warning. If you look at the readme now - it has a Air Humidifer thing in it which :crossed_fingers: should start to bring in data for the humidifier.

I’m working on a bug at the moment, I’ve just noticed on a local server rather than our house system.
The binding has a 10 min gap after start-up, if it authenticates before it starts polling. Our system takes longer to stabilise than this so I hadn’t noticed until now. Please allow it to run for 15 mins then it should be polling frequently for data. I’ll push a new build as soon as I’ve updated the flows.

The new build is uploaded, and so far so good on the long running environment here.

Hi, I’ve installed you latest build from the main and right after installing the bridge (~5sec) I got a good new from the log:
2021-11-09 19:11:43.962 [INFO ] [g.discovery.internal.PersistentInbox] - Added new thing 'vesync:AirHumidifier:99a830a7e5:0f25831e72024bbd9172442c67d25d6e' to inbox.

The thing was already online and seem’s to be ok.

I’ve created a demo items and sitemap file to link all the items, here’s my initial results:

Compared with VeSynch App

  1. power (OK)
  2. displayEna (Not Tested with App)
  3. lowWtrLvl (OK)
  4. hiHumid (Not Tested with App)
  5. tnkRem (Not Tested with App)
  6. stopAtTrg (OK)
  7. humidLvl (OK)
  8. mistLvl (OK, scroll between 1-2-3. Strangely the app as 3 level, 1,5 and 9)
  9. mistLvlVirtual (OK, scroll between 1-5-9. It represent the display In the App)
  10. mode (OK, Auto, manual [No Capital letter?], Sleeping Auto)
  11. lightLvl (OK, scroll between 0-50-100 on each button push from App)
  12. displayEnaCfg (Not Tested with App)
  13. stopAtTrgCfg (Ok, show the target from the App)
  14. humidLvlCfg (Ok, show the target from the App)

Compared with The physical Device

  1. power (OK: Report And Set the Unit)
  2. displayEna (OK: Report And Control. Note: It does follow the same value as displayEnaCfg)
  3. lowWtrLvl (Not Tested, but Should report okay)
  4. hiHumid (Not Tested, need to figure out how to activate)
  5. tnkRem (Ok, Report)
  6. stopAtTrg (Ok, Tested for Setting the Unit)
  7. humidLvl (Ok, Reports only the actual room)
  8. mistLvl (Only Report the actual 1,2,3 State, cannot Set the Unit)
  9. mistLvlVirtual (Report the actual 1,5,9 State, When Sending the Request 1,5,9 The unit does respond and the mistLvl item follow in the 1,2,3 equivalent)
  10. mode (Only Reports, I cannot change the mode of the Unit)
  11. lightLvl (Ok, Provide a fill 0-100% Control, not just a 0,50,100 as the VeSynch App)
  12. displayEnaCfg (Exact Copy of displayEna)
  13. stopAtTrgCfg (Exact Copy of stopAtTrg)
  14. humidLvlCfg (Ok, Set the Unit)

For Mode, the logs give me this error when trying to push a String (Used same string as the one reported):
Humidifier mode command for "Auto" is not valid in the (Classic300S) API

As for the MistLvl I’ve manually set the unit in “Manual” but still the channel only reports and I can’t send a value. Nothing is printed in the log and the item get refreshed with the actual status shown on the unit (1=Low,2=Medium,3=High)

EDIT: Found a way to control the mist level using the Virtual Mist!

Note:

  • If the unit is set in mode=Auto and I change the Virtual Mist, the mode automatically change to manual
  • If the unit is set in mode=Manual and I change the humidLvlCfg, the mode stays in manual and the humidLvlCfg is restored as the next reporting. I would have expected it to change to Auto following the previous bullet point logic

Hi thanks for all the information.

Re. point 10: The reason the mode wasn’t working is that it needs to be all lower case “auto” or “sleep”, like in the sitemap entry below. I’ve updated the binding to change everything to lower-case before it validates the command and sends it. So now if there are capital letters it won’t matter. (I’ve pushed this build just now, to help it handle at least capitals better) :slight_smile:

I have not updated the air purifiers yet, so if anyone is looking at the modes please use see the relevant sitemap examples.

Switch item=LoungeAHMode label=“Mode” mappings=[auto=“Auto”, sleep=“Sleeping”]

I’ll have a read through properly asap and remap a few bits. (Its a bit late here at the moment).

Could you confirm for example item 9. Do you mean there are 3 speed settings on the device. But if you set it to low on the device it shows 1 and can also be set by using 1. Then if you select 2 or medium, it shows as 5 and can be set by using 5. Finally high or 3 is really the value 9? (Just to check I understand correctly).

Many thanks for all the feedback, hopefully I’ll get some time to do a few changes tomorrow after a more awake longer read through. Hopefully this will mean you can set the mode now :crossed_fingers:

Hi, here is the story about mistLvl

Both item channels mistLvl and mistLvlVirtual report the status of the unit and the veSynch App correctly and updates at the same time.
For mistLvl:
1 = Low
2 = Medium
3 = High

For mistLvlVirtual
1 = Low
5 = Medium
9 = High

When Trying to Control the unit from Openhab:
mistLvl doesn’t send any command to the unit (and app) and after the binding refresh it get’s updated to the previous value. Meanwhile mistLvlVirtual doesn’t change

mistLvlVirtual does accept command 1,5 and 9 and the unit (and app) does respond accordingly, the mistLvl channel then get updated at the next binding refresh with the new state. Therefore, only using mistLvlVirtual would work if we know the enumeration mappings=[1=“Minimum”, 5=“Medium”,9=Maximum]

As for the modes, I have a it partially working. Sitemap is setup to see the raw value too:

Selection item=mode mappings=[auto="Auto",manual="Manual",sleep="Sleeping"]
Text item=mode

When changing the mode on the unit (or the app) the sitemap update correctly

From the sitemap, controlling the unit only works for mode auto and sleep. The manual mode doesn’t work and the binding update my item to the previous value. Log is:

[WARN ] [nal.VeSyncDeviceAirHumidifierHandler] - Humidifier mode command for "manual" is not valid in the (Classic300S) API

@lyo125, Thank you :slight_smile: for the clarification on mistLvl, that makes sense. In regards to the modes, in pyvesync it only has auto or sleep. I’m assuming you actually get/see a “manual” response as well?. (It does make sense the air purifier need’s manual mode set first to apply the fan speeds in its case. The binding validates the commands being sent, as pyvesync maps them, so anything other than auto or sleep is not allowed to stop junk going to the vesync servers. I can add it in to the binding, and put a custom build up for you. If you are happy to test then great, but I don’t know what the risks would be as some commands seem to proxy via the web service direct to the device, hence it would have to be at your own risk? I’ll adjust now quickly for the manual mode addition, and make a separate jar. In case you are happy to give it a go.

I’m guessing that hiHumid as well - could be hitHumidiy or HI Humidity. I don’t know which but if your device hits the desired level - could you check what it reports please after a few mins?