Skip to content

Commit 512a905

Browse files
committed
Add Levoit Sprout Air Purifier
Not the complete API set, only basic functions to turn on/off, control mode, fan speed, etc.
1 parent b3f6cfa commit 512a905

File tree

18 files changed

+688
-222
lines changed

18 files changed

+688
-222
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ tools/vesyncdevice.py
3131
pyvesync.und
3232
.venv
3333

34-
34+
models.json
3535
site/
3636
overrides/
3737
*.log

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ dev = [
3535
"pytest",
3636
"pytest-cov",
3737
"pyyaml",
38-
"tox"
38+
"tox",
39+
"aiofiles"
3940
]
4041
docs = [
4142
"mkdocstrings-python",

src/pyvesync/base_devices/purifier_base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,24 @@ class PurifierState(DeviceState):
5454
light_detection_status (str): Light detection status of the purifier.
5555
nightlight_status (str): Nightlight status of the purifier.
5656
fan_rotate_angle (int): Fan rotate angle of the purifier.
57+
temperature (int): Temperature value of the purifier.
58+
humidity (int): Humidity value of the purifier.
59+
voc (int): VOC value of the purifier.
60+
co2 (int): CO2 value of the purifier.
61+
nightlight_brightness (int): Nightlight brightness level of the purifier.
5762
5863
Note:
5964
Not all attributes are supported by all models.
6065
"""
6166

6267
__slots__ = (
68+
6369
"_air_quality_level",
6470
"aq_percent",
6571
"auto_preference_type",
6672
"auto_room_size",
6773
"child_lock",
74+
"co2",
6875
"display_forever",
6976
"display_set_status",
7077
"display_status",
@@ -73,13 +80,17 @@ class PurifierState(DeviceState):
7380
"fan_set_level",
7481
"filter_life",
7582
"filter_open_state",
83+
"humidity",
7684
"light_detection_status",
7785
"light_detection_switch",
7886
"mode",
87+
"nightlight_brightness",
7988
"nightlight_status",
8089
"pm1",
8190
"pm10",
8291
"pm25",
92+
"temperature",
93+
"voc",
8394
)
8495

8596
def __init__(
@@ -102,13 +113,18 @@ def __init__(
102113
self.display_status: str | None = None
103114
self.display_set_status: str | None = None
104115
self.display_forever: bool = False
116+
self.humidity: int | None = None
117+
self.temperature: int | None = None
105118
# Attributes not supported by all purifiers
106119
self.pm25: int | None = None
107120
self.pm1: int | None = None
108121
self.pm10: int | None = None
109122
self.aq_percent: int | None = None
123+
self.voc: int | None = None
124+
self.co2: int | None = None
110125
self.light_detection_switch: str | None = None
111126
self.light_detection_status: str | None = None
127+
self.nightlight_brightness: int | None = None
112128
self.nightlight_status: str | None = None
113129
self.fan_rotate_angle: int | None = None
114130

src/pyvesync/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060

6161
# Generic Constants
6262

63+
KELVIN_MIN = 2700
64+
KELVIN_MAX = 6500
65+
6366

6467
class ProductLines(StrEnum):
6568
"""High level product line."""

src/pyvesync/device_map.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ class ThermostatMap(DeviceMapTemplate):
719719
auto_preferences=[
720720
PurifierAutoPreference.DEFAULT,
721721
PurifierAutoPreference.EFFICIENT,
722-
PurifierAutoPreference.QUIET
722+
PurifierAutoPreference.QUIET,
723723
],
724724
fan_levels=list(range(1, 4)),
725725
nightlight_modes=[NightlightModes.ON, NightlightModes.OFF, NightlightModes.DIM],
@@ -736,12 +736,12 @@ class ThermostatMap(DeviceMapTemplate):
736736
"LAP-C302S-WUSB",
737737
"LAP-C301S-WAAA",
738738
"LAP-C302S-WGC",
739-
],
739+
],
740740
modes=[PurifierModes.SLEEP, PurifierModes.MANUAL, PurifierModes.AUTO],
741741
auto_preferences=[
742742
PurifierAutoPreference.DEFAULT,
743743
PurifierAutoPreference.EFFICIENT,
744-
PurifierAutoPreference.QUIET
744+
PurifierAutoPreference.QUIET,
745745
],
746746
features=[PurifierFeatures.AIR_QUALITY],
747747
fan_levels=list(range(1, 5)),
@@ -760,8 +760,8 @@ class ThermostatMap(DeviceMapTemplate):
760760
auto_preferences=[
761761
PurifierAutoPreference.DEFAULT,
762762
PurifierAutoPreference.EFFICIENT,
763-
PurifierAutoPreference.QUIET
764-
],
763+
PurifierAutoPreference.QUIET,
764+
],
765765
model_display="Core 400S",
766766
model_name="Core 400S",
767767
setup_entry="Core400S",
@@ -774,7 +774,7 @@ class ThermostatMap(DeviceMapTemplate):
774774
auto_preferences=[
775775
PurifierAutoPreference.DEFAULT,
776776
PurifierAutoPreference.EFFICIENT,
777-
PurifierAutoPreference.QUIET
777+
PurifierAutoPreference.QUIET,
778778
],
779779
fan_levels=list(range(1, 5)),
780780
device_alias="Core 600S",
@@ -816,8 +816,8 @@ class ThermostatMap(DeviceMapTemplate):
816816
auto_preferences=[
817817
PurifierAutoPreference.DEFAULT,
818818
PurifierAutoPreference.EFFICIENT,
819-
PurifierAutoPreference.QUIET
820-
],
819+
PurifierAutoPreference.QUIET,
820+
],
821821
model_display="LAP-V102S Series",
822822
model_name="Vital 100S",
823823
setup_entry="LAP-V102S",
@@ -839,17 +839,14 @@ class ThermostatMap(DeviceMapTemplate):
839839
PurifierModes.AUTO,
840840
PurifierModes.PET,
841841
],
842-
features=[
843-
PurifierFeatures.AIR_QUALITY,
844-
PurifierFeatures.LIGHT_DETECT
845-
],
842+
features=[PurifierFeatures.AIR_QUALITY, PurifierFeatures.LIGHT_DETECT],
846843
fan_levels=list(range(1, 5)),
847844
device_alias="Vital 200S",
848845
auto_preferences=[
849846
PurifierAutoPreference.DEFAULT,
850847
PurifierAutoPreference.EFFICIENT,
851-
PurifierAutoPreference.QUIET
852-
],
848+
PurifierAutoPreference.QUIET,
849+
],
853850
model_display="LAP-V201S Series",
854851
model_name="Vital 200S",
855852
setup_entry="LAP-V201S",
@@ -871,19 +868,50 @@ class ThermostatMap(DeviceMapTemplate):
871868
features=[
872869
PurifierFeatures.AIR_QUALITY,
873870
PurifierFeatures.VENT_ANGLE,
874-
PurifierFeatures.LIGHT_DETECT
875-
],
871+
PurifierFeatures.LIGHT_DETECT,
872+
],
876873
fan_levels=list(range(1, 4)),
877874
device_alias="Everest Air",
878875
auto_preferences=[
879876
PurifierAutoPreference.DEFAULT,
880877
PurifierAutoPreference.EFFICIENT,
881-
PurifierAutoPreference.QUIET
882-
],
878+
PurifierAutoPreference.QUIET,
879+
],
883880
model_display="LAP-EL551S Series",
884881
model_name="Everest Air",
885882
setup_entry="EL551S",
886883
),
884+
PurifierMap(
885+
class_name="VeSyncAirBaseV2",
886+
dev_types=[
887+
"LAP-B851S-WEU",
888+
"LAP-B851S-WNA",
889+
"LAP-B851S-AEUR",
890+
"LAP-B851S-AUS",
891+
"LAP-B851S-WUS",
892+
"LAP-BAY-MAX01S",
893+
894+
],
895+
modes=[
896+
PurifierModes.SLEEP,
897+
PurifierModes.MANUAL,
898+
PurifierModes.AUTO,
899+
],
900+
features=[
901+
PurifierFeatures.AIR_QUALITY,
902+
PurifierFeatures.NIGHTLIGHT,
903+
],
904+
fan_levels=list(range(1, 4)),
905+
device_alias="Sprout Air Purifier",
906+
auto_preferences=[
907+
PurifierAutoPreference.DEFAULT,
908+
PurifierAutoPreference.EFFICIENT,
909+
PurifierAutoPreference.QUIET,
910+
],
911+
model_display="Sprout Air Series",
912+
model_name="Sprout Air",
913+
setup_entry="LAP-B851S-WUS",
914+
),
887915
]
888916
"""List of ['PurifierMap'][pyvesync.device_map.PurifierMap] configuration
889917
objects for purifier devices."""

src/pyvesync/devices/vesyncpurifier.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
)
2525
from pyvesync.models.purifier_models import (
2626
PurifierCoreDetailsResult,
27-
PurifierV2DetailsResult,
27+
PurifierVitalDetailsResult,
28+
PurifierSproutResult,
29+
InnerPurifierBaseResult,
2830
PurifierV2EventTiming,
2931
PurifierV2TimerActionItems,
3032
PurifierV2TimerPayloadData,
@@ -457,8 +459,11 @@ def __init__(
457459
"""Initialize the VeSync Base API V2 Air Purifier Class."""
458460
super().__init__(details, manager, feature_map)
459461

460-
def _set_state(self, details: PurifierV2DetailsResult) -> None:
462+
def _set_state(self, details: InnerPurifierBaseResult) -> None:
461463
"""Set Purifier state from details response."""
464+
if not isinstance(details, PurifierVitalDetailsResult):
465+
_LOGGER.debug("Invalid details model passed to _set_state")
466+
return
462467
self.state.connection_status = ConnectionStatus.ONLINE
463468
self.state.device_status = DeviceStatus.from_int(details.powerSwitch)
464469
self.state.mode = details.workMode
@@ -515,7 +520,7 @@ async def get_details(self) -> None:
515520
"""Build API V2 Purifier details dictionary."""
516521
r_dict = await self.call_bypassv2_api('getPurifierStatus')
517522
r_model = process_bypassv2_result(
518-
self, _LOGGER, "get_details", r_dict, PurifierV2DetailsResult
523+
self, _LOGGER, "get_details", r_dict, PurifierVitalDetailsResult
519524
)
520525
if r_model is None:
521526
return
@@ -747,6 +752,90 @@ async def set_mode(self, mode: str) -> bool:
747752
return True
748753

749754

755+
class VeSyncAirSprout(VeSyncAirBaseV2):
756+
"""Class for the Sprout Air Purifier.
757+
758+
Inherits from VeSyncAirBaseV2 class and overrides
759+
the _set_state method.
760+
761+
Args:
762+
details (dict): Dictionary of device details
763+
manager (VeSync): Instantiated VeSync object
764+
feature_map (PurifierMap): Device map template
765+
766+
Attributes:
767+
state (PurifierState): State of the device.
768+
last_response (ResponseInfo): Last response from API call.
769+
manager (VeSync): Manager object for API calls.
770+
device_name (str): Name of device.
771+
device_image (str): URL for device image.
772+
cid (str): Device ID.
773+
connection_type (str): Connection type of device.
774+
device_type (str): Type of device.
775+
type (str): Type of device.
776+
uuid (str): UUID of device, not always present.
777+
config_module (str): Configuration module of device.
778+
mac_id (str): MAC ID of device.
779+
current_firm_version (str): Current firmware version of device.
780+
device_region (str): Region of device. (US, EU, etc.)
781+
pid (str): Product ID of device, pulled by some devices on update.
782+
sub_device_no (int): Sub-device number of device.
783+
product_type (str): Product type of device.
784+
features (dict): Features of device.
785+
modes (list[str]): List of modes supported by the device.
786+
fan_levels (list[int]): List of fan levels supported by the device.
787+
nightlight_modes (list[str]): List of nightlight modes supported by the device.
788+
auto_preferences (list[str]): List of auto preferences supported by the device.
789+
"""
790+
791+
def __init__(
792+
self,
793+
details: ResponseDeviceDetailsModel,
794+
manager: VeSync,
795+
feature_map: PurifierMap,
796+
) -> None:
797+
"""Initialize air purifier class."""
798+
super().__init__(details, manager, feature_map)
799+
800+
def _set_state(self, details: InnerPurifierBaseResult) -> None:
801+
"""Set Purifier state from details response."""
802+
if not isinstance(details, PurifierSproutResult):
803+
_LOGGER.debug("Invalid details model passed to _set_state")
804+
return
805+
self.state.connection_status = ConnectionStatus.ONLINE
806+
self.state.device_status = DeviceStatus.from_int(details.powerSwitch)
807+
self.state.mode = details.workMode
808+
if details.fanSpeedLevel == 255: # noqa: PLR2004
809+
self.state.fan_level = 0
810+
else:
811+
self.state.fan_level = details.fanSpeedLevel
812+
self.state.fan_set_level = details.manualSpeedLevel
813+
self.state.child_lock = bool(details.childLockSwitch)
814+
self.state.air_quality_level = details.AQLevel
815+
self.state.pm25 = details.PM25
816+
self.state.pm1 = details.PM1
817+
self.state.pm10 = details.PM10
818+
self.state.aq_percent = details.AQI
819+
self.state.display_set_status = DeviceStatus.from_int(details.screenSwitch)
820+
self.state.display_status = DeviceStatus.from_int(details.screenState)
821+
auto_pref = details.autoPreference
822+
if auto_pref is not None:
823+
self.state.auto_preference_type = auto_pref.autoPreferenceType
824+
self.state.auto_room_size = auto_pref.roomSize
825+
self.state.humidity = details.humidity
826+
self.state.temperature = int((details.temperature or 0) / 10)
827+
self.state.pm1 = details.PM1
828+
self.state.pm10 = details.PM10
829+
self.state.pm25 = details.PM25
830+
self.state.voc = details.VOC
831+
self.state.co2 = details.CO2
832+
if details.nightlight is not None:
833+
self.state.nightlight_status = DeviceStatus.from_int(
834+
details.nightlight.nightLightSwitch
835+
)
836+
self.state.nightlight_brightness = details.nightlight.brightness
837+
838+
750839
class VeSyncAir131(BypassV1Mixin, VeSyncPurifier):
751840
"""Levoit Air Purifier Class.
752841

0 commit comments

Comments
 (0)