From 85d19e1122df949a195ee3f7818e4ae9a1c83f24 Mon Sep 17 00:00:00 2001 From: s7726 Date: Sun, 9 Aug 2026 10:41:50 -0700 Subject: [PATCH 1/2] WIP: Add support for LUH-N615S-WUS NeoClassic 650s The normal humidifier functions are working. Need to get the "NightLight" working, looks like I need to snoop the API. Also Need to implement the AutoPref types and snoop how to set those. --- docs/supported_devices.md | 20 +- src/pyvesync/base_devices/humidifier_base.py | 5 + src/pyvesync/const.py | 23 +- src/pyvesync/device_map.py | 30 ++ src/pyvesync/devices/vesynchumidifier.py | 263 +++++++++++++++ src/pyvesync/models/humidifier_models.py | 44 +++ .../api/vesynchumidifier/LUH-N651S-WUS.yaml | 300 ++++++++++++++++++ src/tests/call_json_humidifiers.py | 31 ++ 8 files changed, 706 insertions(+), 10 deletions(-) create mode 100644 src/tests/api/vesynchumidifier/LUH-N651S-WUS.yaml diff --git a/docs/supported_devices.md b/docs/supported_devices.md index c80bd7e2..09fa2272 100644 --- a/docs/supported_devices.md +++ b/docs/supported_devices.md @@ -37,6 +37,7 @@ The VeSync API supports a variety of devices. The following is a list of devices - [OasisMist 1000S Humidifier][pyvesync.devices.vesynchumidifier.VeSyncHumid1000S] - [Superior 6000S][pyvesync.devices.vesynchumidifier.VeSyncSuperior6000S] - 6L Smart Humidifier - [Sprout Humidifier][pyvesync.devices.vesynchumidifier.VeSyncSproutHumid] + - [NeoClassic 650s][pyvesync.devices.vesynchumidifier.NeoClassic650s] - 6L Smart Humidifier 6. Fans - [42" Tower Fan][pyvesync.devices.vesyncfan.VeSyncTowerFan] - [Pedestal Fan][pyvesync.devices.vesyncfan.VeSyncPedestalFan] @@ -91,15 +92,16 @@ these methods are used. ### Humidifiers -| Device Name | Night Light | RGB Night Light | Warm Mist | -| ------ | ----- | ----- | ----- | -| Classic 200S | | | | -| Classic 300S | ✔ | | ✔ | -| Dual 200S | | | | -| LV600S | | | ✔ | -| OasisMist 4.5L | | ✔ | ✔ | -| Superior 6000S | | | ✔ | -| Sprout Humidifier | | | | +| Device Name | Nightlight | RGB Nightlight | Warm Mist | Nightlight Color Temp | +| ------ | ----- | ----- | ----- | | +| Classic 200S | | | | | +| Classic 300S | ✔ | | ✔ | | +| Dual 200S | | | | | +| LV600S | | | ✔ | | +| OasisMist 4.5L | | ✔ | ✔ | | +| Superior 6000S | | | ✔ | | +| Sprout Humidifier | | | | | +| NeoClassic 650s | ✔ | | | ✔ | The OasisMist 4.5L (`LUH-O451S-WEU`) exposes an RGB nightlight through [`set_rgb_nightlight`][pyvesync.devices.vesynchumidifier.VeSyncHumid200300S.set_rgb_nightlight]. diff --git a/src/pyvesync/base_devices/humidifier_base.py b/src/pyvesync/base_devices/humidifier_base.py index 7ccfdd99..8012c1a8 100644 --- a/src/pyvesync/base_devices/humidifier_base.py +++ b/src/pyvesync/base_devices/humidifier_base.py @@ -303,6 +303,11 @@ def supports_nightlight_brightness(self) -> bool: """Return True if the humidifier supports nightlight brightness.""" return HumidifierFeatures.NIGHTLIGHT_BRIGHTNESS in self.features + @property + def supports_nightlight_color_temp(self) -> bool: + """Return True if the humidifier supports nightlight brightness.""" + return HumidifierFeatures.NIGHTLIGHT_COLOR_TEMP in self.features + @property def supports_rgb_nightlight(self) -> bool: """Return True if the humidifier supports RGB nightlight. diff --git a/src/pyvesync/const.py b/src/pyvesync/const.py index eb6e02f9..006b132d 100644 --- a/src/pyvesync/const.py +++ b/src/pyvesync/const.py @@ -502,6 +502,7 @@ class HumidifierFeatures(Features): AUTO_STOP: Auto stop when target humidity is reached. Different from auto, which adjusts fan level to maintain humidity. RGB_NIGHTLIGHT: RGB nightlight with color control. + NIGHTLIGHT_COLOR_TEMP: Color Temperature of the Nightlight """ ONOFF = 'onoff' @@ -513,6 +514,7 @@ class HumidifierFeatures(Features): NIGHTLIGHT_BRIGHTNESS = 'nightlight_brightness' DRYING_MODE = 'drying_mode' RGB_NIGHTLIGHT = 'rgb_nightlight' + NIGHTLIGHT_COLOR_TEMP = 'night_light_color_temperature' class PurifierFeatures(Features): @@ -652,7 +654,6 @@ class HumidifierModes(Features): TURBO: Turbo mode. PET: Pet mode. UNKNOWN: Unknown mode. - AUTOPRO: AutoPro mode. """ AUTO = 'auto' @@ -925,3 +926,23 @@ def __int__(self) -> int: DryingModes.RUNNING: 1, DryingModes.PAUSE: 2, } + + +class HumidifierAutoPreference(IntEnum): + """Preference Levels for Humidifier Auto Mode. + + Attributes: + SMART: Smart preference level. + TURBO: Turbo preference level. + MODERATE: Moderate preference level. + GENTLE: Gentle preference level. + """ + + SMART = 3 + TURBO = 5 + MODERATE = 6 + GENTLE = 7 + + def __str__(self) -> str: + """Return string representation of the enum.""" + return self.name.capitalize() diff --git a/src/pyvesync/device_map.py b/src/pyvesync/device_map.py index 172b04c3..804dafc4 100644 --- a/src/pyvesync/device_map.py +++ b/src/pyvesync/device_map.py @@ -71,6 +71,7 @@ FanFeatures, FanModes, FanSleepPreference, + HumidifierAutoPreference, HumidifierFeatures, HumidifierModes, NightlightModes, @@ -278,6 +279,7 @@ class HumidifierMap(DeviceMapTemplate): module: ModuleType = vesynchumidifier target_minmax: tuple[int, int] = (30, 80) warm_mist_levels: list[int] = field(default_factory=list) + auto_preferences: list[HumidifierAutoPreference] = field(default_factory=list) @dataclass(kw_only=True) @@ -809,6 +811,34 @@ class ThermostatMap(DeviceMapTemplate): model_name='Sprout Humidifier', setup_entry='LEH-B381S', ), + HumidifierMap( + class_name='NeoClassic650s', + dev_types=['LUH-N651S-WUS'], + features=[ + HumidifierFeatures.ONOFF, + HumidifierFeatures.AUTO_STOP, + HumidifierFeatures.NIGHTLIGHT, + HumidifierFeatures.NIGHTLIGHT_BRIGHTNESS, + HumidifierFeatures.NIGHTLIGHT_COLOR_TEMP, + ], + mist_modes={ + HumidifierModes.AUTO: 'autoPro', + HumidifierModes.SLEEP: 'sleep', + HumidifierModes.MANUAL: 'manual', + }, + mist_levels=list(range(1, 6)), + auto_preferences=[ + HumidifierAutoPreference.SMART, + HumidifierAutoPreference.TURBO, + HumidifierAutoPreference.MODERATE, + HumidifierAutoPreference.GENTLE, + ], + device_alias='NeoClassic 650s', + model_display='LUH-N651S Series', + model_name='NeoClassic 650s', + setup_entry='LUH-N651S-WUS', + target_minmax=(20, 90), + ), ] """List of ['HumidifierMap'][pyvesync.device_map.HumidifierMap] configuration objects for humidifier devices.""" diff --git a/src/pyvesync/devices/vesynchumidifier.py b/src/pyvesync/devices/vesynchumidifier.py index 99951bd3..a16bae06 100644 --- a/src/pyvesync/devices/vesynchumidifier.py +++ b/src/pyvesync/devices/vesynchumidifier.py @@ -1590,3 +1590,266 @@ async def set_timer(self, duration: int, action: str | None = None) -> bool: remaining=duration, ) return True + + +class NeoClassic650s(BypassV2Mixin, VeSyncHumidifier): + """NeoClassic 650s Humidifier. + + Args: + details (ResponseDeviceDetailsModel): The device details. + manager (VeSync): The manager object for API calls. + feature_map (HumidifierMap): The feature map for the device. + + Attributes: + state (HumidifierState): The state of the humidifier. + last_response (ResponseInfo): Last response from API call. + manager (VeSync): Manager object for API calls. + device_name (str): Name of device. + device_image (str): URL for device image. + cid (str): Device ID. + connection_type (str): Connection type of device. + device_type (str): Type of device. + type (str): Type of device. + uuid (str): UUID of device, not always present. + config_module (str): Configuration module of device. + mac_id (str): MAC ID of device. + current_firm_version (str): Current firmware version of device. + device_region (str): Region of device. (US, EU, etc.) + pid (str): Product ID of device, pulled by some devices on update. + sub_device_no (int): Sub-device number of device. + product_type (str): Product type of device. + features (dict): Features of device. + mist_levels (list): List of mist levels. + mist_modes (list): List of mist modes. + target_minmax (tuple): Tuple of target min and max values. + warm_mist_levels (list): List of warm mist levels. + """ + + __slots__ = () + + def __init__( + self, + details: ResponseDeviceDetailsModel, + manager: VeSync, + feature_map: HumidifierMap, + ) -> None: + """Initialize NeoClassic 650s Humidifier class.""" + super().__init__(details, manager, feature_map) + + def _set_state(self, resp_model: models.NeoClassic650sResult) -> None: + """Set state from NeoClassic 650s API result model.""" + # ** errorCode + # ** errorCodes + # ** lampSwitch + # ** scheduleCount + # ** timerRemain + # ** totalWorkTime + # ** powerSwitch + self.state.device_status = DeviceStatus.from_int(resp_model.powerSwitch) + self.state.connection_status = ConnectionStatus.ONLINE + # ** workMode + self.state.mode = self._reverse_mist_modes.get(resp_model.workMode) + if self.state.mode is None: + logger.warning('Unknown mist mode received: %s', resp_model.workMode) + + # ** targetHumidity + self.state.auto_target_humidity = resp_model.targetHumidity + # ** humidity + self.state.humidity = resp_model.humidity + # ** mistLevel + self.state.mist_level = resp_model.mistLevel + # ** virtualLevel + self.state.mist_virtual_level = resp_model.virtualLevel + # ** waterLacksState + self.state.water_lacks = bool(resp_model.waterLacksState) + # ** waterTankLifted + self.state.water_tank_lifted = bool(resp_model.waterTankLifted) + + # ** autoStopState + # ** autoStopSwitch + self.state.automatic_stop_config = bool(resp_model.autoStopSwitch) + self.state.auto_stop_target_reached = bool(resp_model.autoStopState) + + # ** screenSwitch + self.state.display_set_status = DeviceStatus.from_int(resp_model.screenSwitch) + # ** screenState + self.state.display_status = DeviceStatus.from_int(resp_model.screenState) + + # ** autoPreference + self.state.auto_preference = resp_model.autoPreference + + # ** nightLight + if resp_model.nightLight is not None: + self.state.nightlight_brightness = resp_model.nightLight.brightness + self.state.nightlight_status = DeviceStatus.from_int( + resp_model.nightLight.nightLightSwitch + ) + self.state.nightlight_color_temp = resp_model.nightLight.colorTemperature + # ** temperature + self.state.temperature = ( + resp_model.temperature / 10 + ) # Fahrenheit but without decimals + + async def get_details(self) -> None: + r_dict = await self.call_bypassv2_api('getHumidifierStatus') + r_model = process_bypassv2_result( + self, logger, 'get_details', r_dict, models.NeoClassic650sResult + ) + if r_model is None: + return + + self._set_state(r_model) + + async def toggle_switch(self, toggle: bool | None = None) -> bool: + if toggle is None: + toggle = self.state.device_status != DeviceStatus.ON + + payload_data = {'powerSwitch': int(toggle), 'switchIdx': 0} + r_dict = await self.call_bypassv2_api('setSwitch', payload_data) + r = Helpers.process_dev_response(logger, 'toggle_switch', self, r_dict) + if r is None: + return False + + self.state.device_status = DeviceStatus.from_bool(toggle) + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def toggle_automatic_stop(self, toggle: bool | None = None) -> bool: + if toggle is None: + toggle = self.state.automatic_stop_config is not True + + payload_data = {'autoStopSwitch': int(toggle)} + r_dict = await self.call_bypassv2_api('setAutoStopSwitch', payload_data) + r = Helpers.process_dev_response(logger, 'toggle_automatic_stop', self, r_dict) + if r is None: + return False + + self.state.automatic_stop_config = toggle + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def toggle_display(self, toggle: bool | None = None) -> bool: + if toggle is None: + toggle = self.state.display_set_status != DeviceStatus.ON + + payload_data = {'screenSwitch': int(toggle)} + r_dict = await self.call_bypassv2_api('setDisplay', payload_data) + r = Helpers.process_dev_response(logger, 'set_display', self, r_dict) + if r is None: + return False + + self.state.display_set_status = DeviceStatus.from_bool(toggle) + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def set_humidity(self, humidity: int) -> bool: + if not Validators.validate_range(humidity, *self.target_minmax): + logger.warning( + 'Invalid humidity, must be between %s and %s', *self.target_minmax + ) + return False + + payload_data = {'targetHumidity': humidity} + r_dict = await self.call_bypassv2_api('setTargetHumidity', payload_data) + r = Helpers.process_dev_response(logger, 'set_humidity', self, r_dict) + if r is None: + return False + + self.state.auto_target_humidity = humidity + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def set_mode(self, mode: str) -> bool: + if mode not in self.mist_modes: + logger.warning('Invalid humidity mode used - %s', mode) + logger.info( + 'Proper modes for this device are - %s', + orjson.dumps( + self.mist_modes, + option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, + ), + ) + return False + + payload_data = {'workMode': self.mist_modes[mode]} + r_dict = await self.call_bypassv2_api('setHumidityMode', payload_data) + + r = Helpers.process_dev_response(logger, 'set_humidity_mode', self, r_dict) + if r is None: + return False + + self.state.mode = mode + self.state.device_status = DeviceStatus.ON + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def set_mist_level(self, level: int) -> bool: + if level not in self.mist_levels: + logger.warning( + 'Humidifier mist level, must be between %s and %s', *self.mist_levels + ) + return False + + payload_data = {'levelIdx': 0, 'virtualLevel': level, 'levelType': 'mist'} + r_dict = await self.call_bypassv2_api('setVirtualLevel', payload_data) + r = Helpers.process_dev_response(logger, 'set_mist_level', self, r_dict) + if r is None: + return False + + self.state.mist_level = level + self.state.mist_virtual_level = level + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def toggle_nightlight(self, toggle: bool | None = None) -> bool: + if not self.supports_nightlight: + logger.warning( + '%s is a %s does not have a nightlight or it is not supported.', + self.device_name, + self.device_type, + ) + return False + + if toggle is None: + toggle = self.state.nightlight_status != DeviceStatus.ON + + payload_data = {'nightLightSwitch': int(toggle)} + r_dict = await self.call_bypassv2_api('setNightLightStatus', payload_data) + r = Helpers.process_dev_response(logger, 'toggle_nightlight', self, r_dict) + if r is None: + return False + + self.state.nightlight_status = DeviceStatus.from_bool(toggle) + self.state.connection_status = ConnectionStatus.ONLINE + return True + + async def set_nightlight_brightness(self, brightness: int) -> bool: + if not self.supports_nightlight: + logger.warning( + '%s is a %s does not have a nightlight or it is not supported.', + self.device_name, + self.device_type, + ) + return False + + if not Validators.validate_zero_to_hundred(brightness): + logger.warning('Brightness value must be set between 0 and 100') + return False + + payload_data = { + 'brightness': brightness, + 'nightLightSwitch': 1 if brightness > 0 else 0, + } + r_dict = await self.call_bypassv2_api('setLightStatus', payload_data) + r = Helpers.process_dev_response( + logger, 'set_night_light_brightness', self, r_dict + ) + if r is None: + return False + + self.state.nightlight_brightness = brightness + self.state.nightlight_status = ( + DeviceStatus.ON if brightness > 0 else DeviceStatus.OFF + ) + self.state.connection_status = ConnectionStatus.ONLINE + return True diff --git a/src/pyvesync/models/humidifier_models.py b/src/pyvesync/models/humidifier_models.py index 51424ce4..94e55510 100644 --- a/src/pyvesync/models/humidifier_models.py +++ b/src/pyvesync/models/humidifier_models.py @@ -326,3 +326,47 @@ class LV600SResult(InnerHumidifierBaseResult): totalWorkTime: int = 0 warmPower: bool = False warmLevel: int = 0 + + +# Models for the VeSync NeoClassic 650s Humidifier + + +@dataclass +class NeoClassic650sResult(InnerHumidifierBaseResult): + """NeoClassic 650s Humidifier Result Model. + + Inherits from InnerHumidifierBaseResult. + """ + + autoPreference: int + autoStopState: int + autoStopSwitch: int + errorCode: int + errorCodes: list + humidity: int + lampSwitch: int + mistLevel: int + powerSwitch: int + scheduleCount: int + screenState: int + screenSwitch: int + targetHumidity: int + temperature: int + timerRemain: int + totalWorkTime: int + virtualLevel: int + waterLacksState: int + waterTankLifted: int + workMode: str + nightLight: NeoClassic650sNightlight | None = None + + +@dataclass +class NeoClassic650sNightlight(ResponseBaseModel): + """NeoClassic 650s Humidifier Night Light Model.""" + + nightLightSwitch: int + brightness: int + colorTemperature: int + nightLightLevel: int + brightnessLevel2: int diff --git a/src/tests/api/vesynchumidifier/LUH-N651S-WUS.yaml b/src/tests/api/vesynchumidifier/LUH-N651S-WUS.yaml new file mode 100644 index 00000000..949369ed --- /dev/null +++ b/src/tests/api/vesynchumidifier/LUH-N651S-WUS.yaml @@ -0,0 +1,300 @@ +set_auto_mode: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + workMode: autoPro + method: setHumidityMode + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +set_humidity: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + targetHumidity: 50 + method: setTargetHumidity + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +set_manual_mode: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + workMode: manual + method: setHumidityMode + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +set_mist_level: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + levelIdx: 0 + levelType: mist + virtualLevel: 2 + method: setVirtualLevel + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_off: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + powerSwitch: 0 + switchIdx: 0 + method: setSwitch + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_off_automatic_stop: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + autoStopSwitch: 0 + method: setAutoStopSwitch + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_off_display: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + screenSwitch: 0 + method: setDisplay + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_on: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + powerSwitch: 1 + switchIdx: 0 + method: setSwitch + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_on_automatic_stop: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + autoStopSwitch: 1 + method: setAutoStopSwitch + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +turn_on_display: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: + screenSwitch: 1 + method: setDisplay + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 +update: + headers: + Content-Type: application/json; charset=UTF-8 + User-Agent: okhttp/3.12.1 + json_object: + acceptLanguage: en + accountID: sample_id + appVersion: 5.6.60 + cid: LUH-N651S-WUS-CID + configModel: ConfigModule + configModule: ConfigModule + debugMode: false + deviceId: LUH-N651S-WUS-CID + method: bypassV2 + payload: + data: {} + method: getHumidifierStatus + source: APP + phoneBrand: pyvesync + phoneOS: Android + timeZone: America/New_York + token: sample_tk + traceId: TRACE_ID + userCountryCode: US + method: post + url: /cloud/v2/deviceManaged/bypassV2 diff --git a/src/tests/call_json_humidifiers.py b/src/tests/call_json_humidifiers.py index f7edd911..529b6331 100644 --- a/src/tests/call_json_humidifiers.py +++ b/src/tests/call_json_humidifiers.py @@ -351,6 +351,35 @@ class HumidifierDefaults: "warmPower": HumidifierDefaults.warm_mist_enabled, "warmLevel": HumidifierDefaults.warm_mist_level, }, + "LUH-N651S-WUS": { # NeoClassic 650s + "powerSwitch": int(DeviceStatus.ON), + "humidity": HumidifierDefaults.humidity, + "targetHumidity": HumidifierDefaults.target_humidity, + "virtualLevel": HumidifierDefaults.virtual_mist_level, + "mistLevel": HumidifierDefaults.mist_level, + "workMode": HumidifierDefaults.humidifier_mode.value, + "waterLacksState": int(HumidifierDefaults.water_lacks), + "waterTankLifted": int(HumidifierDefaults.water_tank_lifted), + "autoStopSwitch": int(HumidifierDefaults.auto_stop), + "autoStopState": int(HumidifierDefaults.auto_stop_reached), + "screenSwitch": int(HumidifierDefaults.display_config), + "screenState": int(HumidifierDefaults.display), + "autoPreference": int(HumidifierDefaults.auto_mode_preference), + "temperature": int(HumidifierDefaults.temperature), + "nightLight": { + "nightLightSwitch": int(HumidifierDefaults.nightlight_status), + "brightness": HumidifierDefaults.nightlight_brightness, + "colorTemperature": HumidifierDefaults.nightlight_color_temperature, + "nightLightLevel": 1, + "brightnessLevel2": 100 + }, + "scheduleCount": 0, + "timerRemain": 0, + "errorCode": 0, + "errorCodes": [], + "lampSwitch": 0, + "totalWorkTime": 0, + }, } """This dictionary contains the details response for each humidifier. @@ -368,6 +397,7 @@ class HumidifierDefaults: "LUH-O451S-WEU": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LUH-O451S-WEU"]), "LUH-M101S-WEUR": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LUH-M101S-WEUR"]), "LUH-M101S-WUS": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LUH-M101-WUS"]), + "LUH-N651S-WUS": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LUH-N651S-WUS"]), "LEH-S601S": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LEH-S601S"]), "LEH-B381S": build_bypass_v2_response(inner_result=HUMIDIFIER_DETAILS["LEH-B381S"]), } @@ -393,6 +423,7 @@ class HumidifierDefaults: "LUH-O451S-WEU": deepcopy(FunctionResponsesV2), "LUH-M101S-WEUR": deepcopy(FunctionResponsesV2), "LUH-M101S-WUS": deepcopy(FunctionResponsesV2), + "LUH-N651S-WUS": deepcopy(FunctionResponsesV2), "LEH-S601S": deepcopy(FunctionResponsesV2), "LEH-B381S": deepcopy(FunctionResponsesV2), } From 13d8da57e1e87b0bb58b34a2a3d672a89398c640 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:47:41 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pyvesync/devices/vesynchumidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyvesync/devices/vesynchumidifier.py b/src/pyvesync/devices/vesynchumidifier.py index a16bae06..4a67a44e 100644 --- a/src/pyvesync/devices/vesynchumidifier.py +++ b/src/pyvesync/devices/vesynchumidifier.py @@ -1787,7 +1787,7 @@ async def set_mist_level(self, level: int) -> bool: if level not in self.mist_levels: logger.warning( 'Humidifier mist level, must be between %s and %s', *self.mist_levels - ) + ) return False payload_data = {'levelIdx': 0, 'virtualLevel': level, 'levelType': 'mist'}