From 85fe110bd680e509f2f4ee29b359b87577a1f6f3 Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Sun, 30 Jul 2023 23:08:23 -0400 Subject: [PATCH 1/2] Air Purifier Minor Bug Fixes Remove Vital 100S from 200S config dict entry Fix light detection API call Ensure enabled property is set correctly --- src/pyvesync/vesyncfan.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pyvesync/vesyncfan.py b/src/pyvesync/vesyncfan.py index b8a17ae..7ace81b 100644 --- a/src/pyvesync/vesyncfan.py +++ b/src/pyvesync/vesyncfan.py @@ -106,7 +106,7 @@ 'Vital200S': { 'module': 'VeSyncVital', 'models': ['LAP-V201S-AASR', 'LAP-V201S-WJP', 'LAP-V201S-WEU', - 'LAP-V102S-AUSR', 'LAP-V201S-WUS'], + 'LAP-V201S-WUS', 'LAP-V201-AUSR'], 'modes': ['manual', 'auto', 'sleep', 'off', 'pet'], 'features': ['air_quality'], 'levels': list(range(1, 5)) @@ -783,6 +783,7 @@ def get_details(self) -> None: if outer_result.get('code') == 0: self.build_purifier_dict(inner_result) else: + self.online = False logger.debug('error in inner result dict from purifier') if inner_result.get('configuration', {}): self.build_config_dict(inner_result.get('configuration', {})) @@ -808,9 +809,10 @@ def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: def build_purifier_dict(self, dev_dict: dict) -> None: """Build Bypass purifier status dictionary.""" - power_switch = dev_dict.get('power_switch', 0) + self.online = True + power_switch = bool(dev_dict.get('powerSwitch', 0)) self.enabled = power_switch - self.device_status = 'on' if power_switch else 'off' + self.device_status = 'on' if power_switch is True else 'off' self.mode = dev_dict.get('workMode', 'manual') self.speed = dev_dict.get('fanSpeedLevel', 0) @@ -818,16 +820,12 @@ def build_purifier_dict(self, dev_dict: dict) -> None: self.set_speed_level = dev_dict.get('manualSpeedLevel', 1) self.details['filter_life'] = dev_dict.get('filterLifePercent', 0) - self.details['display'] = dev_dict.get('display', False) self.details['child_lock'] = bool(dev_dict.get('childLockSwitch', 0)) - self.details['night_light'] = dev_dict.get('night_light', 'off') self.details['display'] = bool(dev_dict.get('screenState', 0)) - self.details['display_forever'] = dev_dict.get('display_forever', False) self.details['light_detection_switch'] = bool( dev_dict.get('lightDetectionSwitch', 0)) self.details['environment_light_state'] = bool( dev_dict.get('environmentLightState', 0)) - self.details['screen_state'] = bool(dev_dict.get('screenState', 0)) self.details['screen_switch'] = bool(dev_dict.get('screenSwitch', 0)) if self.air_quality_feature is True: @@ -856,7 +854,7 @@ def set_light_detection(self, toggle: bool) -> bool: logger.debug("Light Detection is already set to %s", toggle_id) return True - head, body = self.build_api_dict('setLightDetectionSwitch') + head, body = self.build_api_dict('setLightDetection') body['payload']['data']['lightDetectionSwitch'] = toggle_id r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', @@ -914,7 +912,7 @@ def toggle_switch(self, toggle: bool) -> bool: return False def set_child_lock(self, mode: bool) -> bool: - """Levoit 100S set Child Lock.""" + """Levoit 100S/200S set Child Lock.""" if mode: toggle_id = 1 else: From fac605a8066ae81e6a8c69e9138eb706462f4116 Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Tue, 8 Aug 2023 21:22:05 -0400 Subject: [PATCH 2/2] Fix display & enabled/disabled bug --- MANIFEST.in | 2 + setup.py | 5 +-- src/pyvesync/helpers.py | 13 +++++++ src/pyvesync/vesyncfan.py | 78 +++++++++++++++++++++++---------------- 4 files changed, 63 insertions(+), 35 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index b2cc4f5..16b5b8f 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ include README.md LICENSE requirements.txt +exclude test +prune test \ No newline at end of file diff --git a/setup.py b/setup.py index 1f811a6..414bb61 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='pyvesync', - version='2.1.8', + version='2.1.9', description='pyvesync is a library to manage Etekcity\ Devices, Cosori Air Fryers and Levoit Air \ Purifiers run on the VeSync app.', @@ -28,9 +28,8 @@ 'Programming Language :: Python :: 3.8', ], keywords=['iot', 'vesync', 'levoit', 'etekcity', 'cosori', 'valceno'], - packages=find_packages('src', exclude=['tests', 'tests.*']), + packages=find_packages('src', exclude=['tests', 'test*']), package_dir={'': 'src'}, - zip_safe=False, install_requires=['requests>=2.20.0'], extras_require={ 'dev': ['pytest', 'pytest-cov', 'yaml', 'tox'] diff --git a/src/pyvesync/helpers.py b/src/pyvesync/helpers.py index 94ca05a..d318172 100644 --- a/src/pyvesync/helpers.py +++ b/src/pyvesync/helpers.py @@ -194,6 +194,19 @@ def redactor(cls, stringvalue: str) -> str: '##_REDACTED_##', stringvalue) return stringvalue + @staticmethod + def nested_code_check(response: dict) -> bool: + """Return true if all code values are 0.""" + if isinstance(response, dict): + for key, value in response.items(): + if key == 'code': + if value != 0: + return False + elif isinstance(value, dict): + if not Helpers.nested_code_check(value): + return False + return True + @staticmethod def call_api(api: str, method: str, json_object: Optional[dict] = None, headers: Optional[dict] = None) -> tuple: diff --git a/src/pyvesync/vesyncfan.py b/src/pyvesync/vesyncfan.py index 7ace81b..96e4b7e 100644 --- a/src/pyvesync/vesyncfan.py +++ b/src/pyvesync/vesyncfan.py @@ -99,7 +99,7 @@ 'module': 'VeSyncVital', 'models': ['LAP-V102S-AASR', 'LAP-V102S-WUS', 'LAP-V102S-WEU', 'LAP-V102S-AUSR', 'LAP-V102S-WJP'], - 'modes': ['manual', 'auto', 'sleep', 'off'], + 'modes': ['manual', 'auto', 'sleep', 'off', 'pet'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, @@ -768,29 +768,20 @@ def get_details(self) -> None: headers=head, json_object=body, ) - if not isinstance(r, dict): - logger.debug('Error in purifier response') + if Helpers.nested_code_check(r) is False or not isinstance(r, dict): + logger.debug('Error getting purifier details') + self.connection_status = 'offline' return - if not isinstance(r.get('result'), dict): - logger.debug('Error in purifier response') - return - outer_result = r.get('result', {}) - inner_result = None - if outer_result: - inner_result = r.get('result', {}).get('result') - if inner_result is not None and Helpers.code_check(r): - if outer_result.get('code') == 0: - self.build_purifier_dict(inner_result) - else: - self.online = False - logger.debug('error in inner result dict from purifier') - if inner_result.get('configuration', {}): - self.build_config_dict(inner_result.get('configuration', {})) - else: - logger.debug('No configuration found in purifier status') + inner_result = r.get('result', {}).get('result') + + if inner_result is not None: + self.build_purifier_dict(inner_result) else: - logger.debug('Error in purifier response') + self.connection_status = 'offline' + logger.debug('error in inner result dict from purifier') + if inner_result.get('configuration', {}): + self.build_config_dict(inner_result.get('configuration', {})) def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: """Return default body for Levoit Vital 100S/200S API.""" @@ -809,7 +800,7 @@ def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: def build_purifier_dict(self, dev_dict: dict) -> None: """Build Bypass purifier status dictionary.""" - self.online = True + self.connection_status = 'online' power_switch = bool(dev_dict.get('powerSwitch', 0)) self.enabled = power_switch self.device_status = 'on' if power_switch is True else 'off' @@ -846,10 +837,7 @@ def pet_mode(self) -> bool: def set_light_detection(self, toggle: bool) -> bool: """Enable/Disable Light Detection Feature.""" - if toggle is True: - toggle_id = 1 - else: - toggle_id = 0 + toggle_id = int(toggle) if self.details['light_detection_switch'] == toggle_id: logger.debug("Light Detection is already set to %s", toggle_id) return True @@ -863,7 +851,7 @@ def set_light_detection(self, toggle: bool) -> bool: json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): self.details['light_detection'] = toggle return True logger.debug("Error toggling purifier - %s", @@ -901,7 +889,7 @@ def toggle_switch(self, toggle: bool) -> bool: json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): if toggle: self.device_status = 'on' else: @@ -929,7 +917,7 @@ def set_child_lock(self, mode: bool) -> bool: json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): self.details['child_lock'] = mode return True @@ -954,7 +942,7 @@ def set_display(self, mode: bool) -> bool: json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): self.details['screen_switch'] = mode return True @@ -1004,7 +992,7 @@ def set_timer(self, timer_duration: int, action: str = 'off', json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): self.timer = Timer(timer_duration, action) return True @@ -1024,7 +1012,7 @@ def clear_timer(self) -> bool: json_object=body, ) - if r is not None and Helpers.code_check(r): + if r is not None and Helpers.nested_code_check(r): self.timer = None return True @@ -1164,6 +1152,32 @@ def mode_toggle(self, mode: str) -> bool: logger.debug('Error setting purifier mode') return False + def displayJSON(self) -> str: + """Return air purifier status and properties in JSON output.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + 'Mode': self.mode, + 'Filter Life': str(self.details['filter_life']), + 'Fan Level': str(self.speed), + 'Display On': self.details['display'], + 'Child Lock': self.details['child_lock'], + 'Night Light': str(self.details['night_light']), + 'Display Set On': self.details['screen_switch'], + 'Light Detection Enabled': self.details['light_detection_switch'], + 'Environment Light State': self.details['environment_light_state'] + } + ) + if self.air_quality_feature is True: + sup_val.update( + {'Air Quality Level': str(self.details.get('air_quality', ''))} + ) + sup_val.update( + {'Air Quality Value': str(self.details.get('air_quality_value', ''))} + ) + return json.dumps(sup_val, indent=4) + class VeSyncAir131(VeSyncBaseDevice): """Levoit Air Purifier Class."""