From d698a072d2914c40714dafddfefcce161dd8b301 Mon Sep 17 00:00:00 2001 From: Ben Hamilton Date: Sun, 31 Oct 2021 15:11:04 -0600 Subject: [PATCH 1/4] Add support for Dual 200S humidifier device --- src/pyvesync/vesync.py | 4 +- src/pyvesync/vesyncfan.py | 344 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/src/pyvesync/vesync.py b/src/pyvesync/vesync.py index 4d70508..f258b2a 100755 --- a/src/pyvesync/vesync.py +++ b/src/pyvesync/vesync.py @@ -11,6 +11,7 @@ from pyvesync.vesyncbulb import VeSyncBulbESL100, VeSyncBulbESL100CW from pyvesync.vesyncfan import ( VeSyncAir131, + VeSyncHumid200S, VeSyncHumid300S, VeSyncAir200S, VeSyncAir300S400S, @@ -45,6 +46,7 @@ 'ESL100CW': VeSyncBulbESL100CW, 'ESWD16': VeSyncDimmerSwitch, 'Classic300S': VeSyncHumid300S, + 'Dual200S': VeSyncHumid200S, 'Core200S': VeSyncAir200S, 'Core300S': VeSyncAir300S400S, 'Core400S': VeSyncAir300S400S, @@ -54,7 +56,7 @@ outlets=['wifi-switch-1.3', 'ESW03-USA', 'ESW01-EU', 'ESW15-USA', 'ESO15-TB'], switches=['ESWL01', 'ESWL03', 'ESWD16'], - fans=['LV-PUR131S', 'Classic300S', 'Core200S', 'Core300S', 'Core400S'], + fans=['LV-PUR131S', 'Classic300S', 'Core200S', 'Core300S', 'Core400S', 'Dual200S'], bulbs=['ESL100', 'ESL100CW'], ) diff --git a/src/pyvesync/vesyncfan.py b/src/pyvesync/vesyncfan.py index 00f6946..9070862 100644 --- a/src/pyvesync/vesyncfan.py +++ b/src/pyvesync/vesyncfan.py @@ -1434,3 +1434,347 @@ def displayJSON(self) -> str: } ) return json.dumps(sup_val) + + +class VeSyncHumid200S(VeSyncBaseDevice): + """200S Humidifier Class.""" + + def __init__(self, details, manager): + """Initialize 200S Humidifier class.""" + super().__init__(details, manager) + self.enabled = True + self.details: Dict[str, Union[str, int, float]] = { + 'humidity': 0, + 'mist_virtual_level': 0, + 'mist_level': 0, + 'mode': 'manual', + 'water_lacks': False, + 'humidity_high': False, + 'water_tank_lifted': False, + 'display': False, + 'automatic_stop_reach_target': False + } + self.config: Dict[str, Union[str, int, float]] = { + 'auto_target_humidity': 0, + 'display': False, + 'automatic_stop': True + } + + def __build_api_dict(self, method: str) -> Tuple[Dict, Dict]: + """Build 200S api call header and body. + + Available methods are: 'getHumidifierStatus', 'setAutomaticStop', + 'setSwitch', 'setVirtualLevel', + 'setTargetHumidity', 'setHumidityMode' + """ + modes = ['getHumidifierStatus', 'setAutomaticStop', + 'setSwitch', 'setVirtualLevel', + 'setTargetHumidity', 'setHumidityMode', 'setDisplay'] + if method not in modes: + logger.debug('Invalid mode - %s', method) + return {}, {} + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body['cid'] = self.cid + body['configModule'] = self.config_module + body['payload'] = { + 'method': method, + 'source': 'APP' + } + return head, body + + def build_humid_dict(self, dev_dict: Dict): + """Build 200S humidifier status dictionary.""" + self.enabled = dev_dict.get('enabled') + self.details['humidity'] = dev_dict.get('humidity', 0) + self.details['mist_virtual_level'] = dev_dict.get( + 'mist_virtual_level', 0) + self.details['mist_level'] = dev_dict.get('mist_level', 0) + self.details['mode'] = dev_dict.get('mode', 'manual') + self.details['water_lacks'] = dev_dict.get('water_lacks', False) + self.details['humidity_high'] = dev_dict.get('humidity_high', False) + self.details['water_tank_lifted'] = dev_dict.get( + 'water_tank_lifted', False) + self.details['display'] = dev_dict.get('display', False) + self.details['automatic_stop_reach_target'] = dev_dict.get( + 'automatic_stop_reach_target', True + ) + + def build_config_dict(self, conf_dict): + """Build configuration dict for 300s humidifier.""" + self.config['auto_target_humidity'] = conf_dict.get( + 'auto_target_humidity', 0) + self.config['display'] = conf_dict.get('display', False) + self.config['automatic_stop'] = conf_dict.get('automatic_stop', True) + + def get_details(self) -> None: + """Build 200S Humidifier details dictionary.""" + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body['cid'] = self.cid + body['configModule'] = self.config_module + body['payload'] = { + 'method': 'getHumidifierStatus', + 'source': 'APP', + 'data': {} + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + outer_result = r.get('result', {}) + inner_result = None + + if outer_result is not None: + 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_humid_dict(inner_result) + else: + logger.debug('error in inner result dict from humidifier') + if inner_result.get('configuration', {}): + self.build_config_dict(inner_result.get('configuration', {})) + else: + logger.debug('No configuration found in humidifier status') + else: + logger.debug('Error in humidifier response') + + def update(self): + """Update 200S Humidifier details.""" + self.get_details() + + def toggle_switch(self, toggle: bool) -> bool: + """Toggle humidifier on/off.""" + if not isinstance(toggle, bool): + logger.debug('Invalid toggle value for humidifier switch') + return False + + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body['cid'] = self.cid + body['configModule'] = self.config_module + body['payload'] = { + 'data': { + 'enabled': toggle, + 'id': 0 + }, + 'method': 'setSwitch', + 'source': 'APP' + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug("Error toggling 200S humidifier - %s", self.device_name) + return False + + def turn_on(self) -> bool: + """Turn 200S Humidifier on.""" + return self.toggle_switch(True) + + def turn_off(self): + """Turn 200S Humidifier off.""" + return self.toggle_switch(False) + + def automatic_stop_on(self) -> bool: + """Turn 200S Humidifier automatic stop on.""" + return self.set_automatic_stop(True) + + def automatic_stop_off(self) -> bool: + """Turn 200S Humidifier automatic stop on.""" + return self.set_automatic_stop(False) + + def set_automatic_stop(self, mode: bool) -> bool: + """Set 200S Humidifier to automatic stop.""" + if mode not in (True, False): + logger.debug( + 'Invalid mode passed to set_automatic_stop - %s', mode) + return False + + head, body = self.__build_api_dict('setAutomaticStop') + if not head and not body: + return False + + body['payload']['data'] = { + 'enabled': mode + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + if isinstance(r, dict): + logger.debug('Error toggling automatic stop') + else: + logger.debug('Error in api return json for %s', self.device_name) + return False + + def set_display(self, mode: bool) -> bool: + """Toggle display on/off.""" + if not isinstance(mode, bool): + logger.debug("Mode must be True or False") + return False + + head, body = self.__build_api_dict('setDisplay') + + body['payload']['data'] = { + 'state': mode + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug("Error toggling 200S display - %s", self.device_name) + return False + + def turn_on_display(self) -> bool: + """Turn 200S Humidifier on.""" + return self.set_display(True) + + def turn_off_display(self): + """Turn 200S Humidifier off.""" + return self.set_display(False) + + def set_humidity(self, humidity: int) -> bool: + """Set target 200S Humidifier humidity.""" + if humidity < 30 or humidity > 80: + logger.debug("Humidity value must be set between 30 and 80") + return False + head, body = self.__build_api_dict('setTargetHumidity') + + if not head and not body: + return False + + body['payload']['data'] = { + 'target_humidity': humidity + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug('Error setting humidity') + return False + + def set_humidity_mode(self, mode: str) -> bool: + """Set humidifier mode - sleep or auto.""" + if mode.lower() not in ['sleep', 'auto']: + logger.debug('Invalid humidity mode used (sleep or auto)- %s', + mode) + return False + head, body = self.__build_api_dict('setHumidityMode') + if not head and not body: + return False + body['payload']['data'] = { + 'mode': mode.lower() + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug('Error setting humidity mode') + return False + + def set_mist_level(self, level: int) -> bool: + """Set humidifier mist level with int between 0 - 9.""" + if level < 1 or level > 9: + logger.debug('Humidifier mist level must be between 0 and 9') + return False + + head, body = self.__build_api_dict('setVirtualLevel') + if not head and not body: + return False + + body['payload']['data'] = { + 'id': 0, + 'level': level, + 'type': 'mist' + } + + r, _ = Helpers.call_api( + '/cloud/v2/deviceManaged/bypassV2', + method='post', + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug('Error setting mist level') + return False + + def display(self) -> None: + """Return formatted device info to stdout.""" + super().display() + disp1 = [ + ('Mode: ', self.details['mode'], ''), + ('Humidity: ', self.details['humidity'], 'percent'), + ('Mist Virtual Level: ', self.details['mist_virtual_level'], ''), + ('Mist Level: ', self.details['mist_level'], ''), + ('Water Lacks: ', self.details['water_lacks'], ''), + ('Humidity High: ', self.details['humidity_high'], ''), + ('Water Tank Lifted: ', self.details['water_tank_lifted'], ''), + ('Display: ', self.details['display'], ''), + ('Automatic Stop Reach Target: ', + self.details['automatic_stop_reach_target'], ''), + ('Auto Target Humidity: ', + self.config['auto_target_humidity'], 'percent'), + ('Automatic Stop: ', self.config['automatic_stop'], ''), + ] + for line in disp1: + print(f'{line[0]:.<29} {line[1]} {line[2]}') + + def displayJSON(self) -> str: + """Return humidifier status and properties in JSON output.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + 'Mode': self.details['mode'], + 'Humidity': str(self.details['humidity']), + 'Mist Virtual Level': str( + self.details['mist_virtual_level']), + 'Mist Level': str(self.details['mist_level']), + 'Water Lacks': self.details['water_lacks'], + 'Humidity High': self.details['humidity_high'], + 'Water Tank Lifted': self.details['water_tank_lifted'], + 'Display': self.details['display'], + 'Automatic Stop Reach Target': self.details[ + 'automatic_stop_reach_target'], + 'Auto Target Humidity': str(self.config[ + 'auto_target_humidity']), + 'Automatic Stop': self.config['automatic_stop'], + } + ) + return json.dumps(sup_val) From aae6661588fef220ef78c8a2d449c7de9db0e945 Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Wed, 17 Nov 2021 22:21:06 -0500 Subject: [PATCH 2/4] Version Bump --- azure-pipelines.yml | 2 +- setup.py | 2 +- src/pyvesync/vesync.py | 15 +- src/pyvesync/vesyncbasedevice.py | 6 +- src/pyvesync/vesyncfan.py | 406 +++---------------------------- 5 files changed, 53 insertions(+), 378 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0046ddf..6d3341d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -39,7 +39,7 @@ jobs: - job: 'pytest' pool: - vmImage: 'ubuntu-latest' + vmImage: 'ubuntu-20.04' strategy: matrix: Python36: diff --git a/setup.py b/setup.py index 1e1f869..35db0bb 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='pyvesync', - version='1.4.1', + version='1.4.2', description='pyvesync is a library to manage Etekcity\ Devices and Levoit Air Purifier', long_description=long_description, diff --git a/src/pyvesync/vesync.py b/src/pyvesync/vesync.py index f258b2a..92c210a 100755 --- a/src/pyvesync/vesync.py +++ b/src/pyvesync/vesync.py @@ -5,14 +5,14 @@ import time from itertools import chain from collections import defaultdict -from typing import List, Dict, DefaultDict, Union, Any +from typing import List, Dict, DefaultDict, Union, Any, Type from pyvesync.helpers import Helpers +from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.vesyncbulb import VeSyncBulbESL100, VeSyncBulbESL100CW from pyvesync.vesyncfan import ( VeSyncAir131, - VeSyncHumid200S, - VeSyncHumid300S, + VeSyncHumid200300S, VeSyncAir200S, VeSyncAir300S400S, ) @@ -33,7 +33,7 @@ # Class dictionary based on device type -_DEVICE_CLASS: Dict[str, Any] = { +_DEVICE_CLASS: Dict[str, Type[VeSyncBaseDevice]] = { 'wifi-switch-1.3': VeSyncOutlet7A, 'ESW03-USA': VeSyncOutlet10A, 'ESW01-EU': VeSyncOutlet10A, @@ -45,8 +45,8 @@ 'ESL100': VeSyncBulbESL100, 'ESL100CW': VeSyncBulbESL100CW, 'ESWD16': VeSyncDimmerSwitch, - 'Classic300S': VeSyncHumid300S, - 'Dual200S': VeSyncHumid200S, + 'Classic300S': VeSyncHumid200300S, + 'Dual200S': VeSyncHumid200300S, 'Core200S': VeSyncAir200S, 'Core300S': VeSyncAir300S400S, 'Core400S': VeSyncAir300S400S, @@ -56,7 +56,8 @@ outlets=['wifi-switch-1.3', 'ESW03-USA', 'ESW01-EU', 'ESW15-USA', 'ESO15-TB'], switches=['ESWL01', 'ESWL03', 'ESWD16'], - fans=['LV-PUR131S', 'Classic300S', 'Core200S', 'Core300S', 'Core400S', 'Dual200S'], + fans=['LV-PUR131S', 'Classic300S', 'Core200S', + 'Core300S', 'Core400S', 'Dual200S'], bulbs=['ESL100', 'ESL100CW'], ) diff --git a/src/pyvesync/vesyncbasedevice.py b/src/pyvesync/vesyncbasedevice.py index 648f7bb..d17eb7f 100644 --- a/src/pyvesync/vesyncbasedevice.py +++ b/src/pyvesync/vesyncbasedevice.py @@ -52,8 +52,10 @@ def __hash__(self): def __str__(self): """Use device info for string represtation of class.""" - return f'Device Name: {self.device_name}, Device Type: {self.device_type},\ - SubDevice No.: {self.sub_device_no} Status: {self.device_status}' + return f'Device Name: {self.device_name}, \ + Device Type: {self.device_type},\ + SubDevice No.: {self.sub_device_no},\ + Status: {self.device_status}' def __repr__(self): """Representation of device details.""" diff --git a/src/pyvesync/vesyncfan.py b/src/pyvesync/vesyncfan.py index 9070862..d95d5bd 100644 --- a/src/pyvesync/vesyncfan.py +++ b/src/pyvesync/vesyncfan.py @@ -7,6 +7,10 @@ from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.helpers import Helpers +air_features = { + 'Dual200S': [], + 'Classic300S': ['nightlight'] +} logger = logging.getLogger(__name__) @@ -1059,13 +1063,17 @@ def displayJSON(self) -> str: return sup_val -class VeSyncHumid300S(VeSyncBaseDevice): - """300S Humidifier Class.""" +class VeSyncHumid200300S(VeSyncBaseDevice): + """200S/300S Humidifier Class.""" def __init__(self, details, manager): - """Initilize 300S Humidifier class.""" + """Initilize 200S/300S Humidifier class.""" super().__init__(details, manager) self.enabled = True + if 'nightlight' in air_features.get(details.type): + self.night_light = True + else: + self.night_light = False self.details: Dict[str, Union[str, int, float]] = { 'humidity': 0, 'mist_virtual_level': 0, @@ -1076,8 +1084,9 @@ def __init__(self, details, manager): 'water_tank_lifted': False, 'display': False, 'automatic_stop_reach_target': False, - 'night_light_brightness': 0 } + if self.night_light: + self.details['night_light_brightness'] = 0 self.config: Dict[str, Union[str, int, float]] = { 'auto_target_humidity': 0, 'display': False, @@ -1085,7 +1094,7 @@ def __init__(self, details, manager): } def __build_api_dict(self, method: str) -> Tuple[Dict, Dict]: - """Build 300S api call header and body. + """Build 200S/300S api call header and body. Available methods are: 'getHumidifierStatus', 'setAutomaticStop', 'setSwitch', 'setNightLightBrightness', 'setVirtualLevel', @@ -1108,7 +1117,7 @@ def __build_api_dict(self, method: str) -> Tuple[Dict, Dict]: return head, body def build_humid_dict(self, dev_dict: Dict): - """Build 300S humidifier status dictionary.""" + """Build 200S/300S humidifier status dictionary.""" self.enabled = dev_dict.get('enabled') self.details['humidity'] = dev_dict.get('humidity', 0) self.details['mist_virtual_level'] = dev_dict.get( @@ -1123,8 +1132,9 @@ def build_humid_dict(self, dev_dict: Dict): self.details['automatic_stop_reach_target'] = dev_dict.get( 'automatic_stop_reach_target', True ) - self.details['night_light_brightness'] = dev_dict.get( - 'night_light_brightness', 0) + if self.night_light: + self.details['night_light_brightness'] = dev_dict.get( + 'night_light_brightness', 0) def build_config_dict(self, conf_dict): """Build configuration dict for 300s humidifier.""" @@ -1134,7 +1144,7 @@ def build_config_dict(self, conf_dict): self.config['automatic_stop'] = conf_dict.get('automatic_stop', True) def get_details(self) -> None: - """Build 300S Humidifier details dictionary.""" + """Build 200S/300S Humidifier details dictionary.""" head = Helpers.bypass_header() body = Helpers.bypass_body_v2(self.manager) body['cid'] = self.cid @@ -1169,7 +1179,7 @@ def get_details(self) -> None: logger.debug('Error in humidifier response') def update(self): - """Update 300S Humidifier details.""" + """Update 200S/300S Humidifier details.""" self.get_details() def toggle_switch(self, toggle: bool) -> bool: @@ -1204,23 +1214,23 @@ def toggle_switch(self, toggle: bool) -> bool: return False def turn_on(self) -> bool: - """Turn 300S Humidifier on.""" + """Turn 200S/300S Humidifier on.""" return self.toggle_switch(True) def turn_off(self): - """Turn 300S Humidifier off.""" + """Turn 200S/300S Humidifier off.""" return self.toggle_switch(False) def automatic_stop_on(self) -> bool: - """Turn 300S Humidifier automatic stop on.""" + """Turn 200S/300S Humidifier automatic stop on.""" return self.set_automatic_stop(True) def automatic_stop_off(self) -> bool: - """Turn 300S Humidifier automatic stop on.""" + """Turn 200S/300S Humidifier automatic stop on.""" return self.set_automatic_stop(False) def set_automatic_stop(self, mode: bool) -> bool: - """Set 300S Humidifier to automatic stop.""" + """Set 200S/300S Humidifier to automatic stop.""" if mode not in (True, False): logger.debug( 'Invalid mode passed to set_automatic_stop - %s', mode) @@ -1274,15 +1284,15 @@ def set_display(self, mode: bool) -> bool: return False def turn_on_display(self) -> bool: - """Turn 300S Humidifier on.""" + """Turn 200S/300S Humidifier on.""" return self.set_display(True) def turn_off_display(self): - """Turn 300S Humidifier off.""" + """Turn 200S/300S Humidifier off.""" return self.set_display(False) def set_humidity(self, humidity: int) -> bool: - """Set target 300S Humidifier humidity.""" + """Set target 200S/300S Humidifier humidity.""" if humidity < 30 or humidity > 80: logger.debug("Humidity value must be set between 30 and 80") return False @@ -1308,7 +1318,11 @@ def set_humidity(self, humidity: int) -> bool: return False def set_night_light_brightness(self, brightness: int) -> bool: - """Set target 300S Humidifier night light brightness.""" + """Set target 200S/300S Humidifier night light brightness.""" + if not self.night_light: + logger.debug('%s is a %s does not have a nightlight', + self.device_name, self.device_type) + return False if brightness < 0 or brightness > 100: logger.debug("Brightness value must be set between 0 and 100") return False @@ -1400,12 +1414,13 @@ def display(self) -> None: ('Display: ', self.details['display'], ''), ('Automatic Stop Reach Target: ', self.details['automatic_stop_reach_target'], ''), - ('Night Light Brightness: ', - self.details['night_light_brightness'], 'percent'), ('Auto Target Humidity: ', self.config['auto_target_humidity'], 'percent'), ('Automatic Stop: ', self.config['automatic_stop'], ''), ] + if self.night_light: + disp1.append(('Night Light Brightness: ', + self.details['night_light_brightness'], 'percent')) for line in disp1: print(f'{line[0]:.<29} {line[1]} {line[2]}') @@ -1413,352 +1428,6 @@ 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.details['mode'], - 'Humidity': str(self.details['humidity']), - 'Mist Virtual Level': str( - self.details['mist_virtual_level']), - 'Mist Level': str(self.details['mist_level']), - 'Water Lacks': self.details['water_lacks'], - 'Humidity High': self.details['humidity_high'], - 'Water Tank Lifted': self.details['water_tank_lifted'], - 'Display': self.details['display'], - 'Automatic Stop Reach Target': self.details[ - 'automatic_stop_reach_target'], - 'Night Light Brightness': self.details[ - 'night_light_brightness'], - 'Auto Target Humidity': str(self.config[ - 'auto_target_humidity']), - 'Automatic Stop': self.config['automatic_stop'], - } - ) - return json.dumps(sup_val) - - -class VeSyncHumid200S(VeSyncBaseDevice): - """200S Humidifier Class.""" - - def __init__(self, details, manager): - """Initialize 200S Humidifier class.""" - super().__init__(details, manager) - self.enabled = True - self.details: Dict[str, Union[str, int, float]] = { - 'humidity': 0, - 'mist_virtual_level': 0, - 'mist_level': 0, - 'mode': 'manual', - 'water_lacks': False, - 'humidity_high': False, - 'water_tank_lifted': False, - 'display': False, - 'automatic_stop_reach_target': False - } - self.config: Dict[str, Union[str, int, float]] = { - 'auto_target_humidity': 0, - 'display': False, - 'automatic_stop': True - } - - def __build_api_dict(self, method: str) -> Tuple[Dict, Dict]: - """Build 200S api call header and body. - - Available methods are: 'getHumidifierStatus', 'setAutomaticStop', - 'setSwitch', 'setVirtualLevel', - 'setTargetHumidity', 'setHumidityMode' - """ - modes = ['getHumidifierStatus', 'setAutomaticStop', - 'setSwitch', 'setVirtualLevel', - 'setTargetHumidity', 'setHumidityMode', 'setDisplay'] - if method not in modes: - logger.debug('Invalid mode - %s', method) - return {}, {} - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body['cid'] = self.cid - body['configModule'] = self.config_module - body['payload'] = { - 'method': method, - 'source': 'APP' - } - return head, body - - def build_humid_dict(self, dev_dict: Dict): - """Build 200S humidifier status dictionary.""" - self.enabled = dev_dict.get('enabled') - self.details['humidity'] = dev_dict.get('humidity', 0) - self.details['mist_virtual_level'] = dev_dict.get( - 'mist_virtual_level', 0) - self.details['mist_level'] = dev_dict.get('mist_level', 0) - self.details['mode'] = dev_dict.get('mode', 'manual') - self.details['water_lacks'] = dev_dict.get('water_lacks', False) - self.details['humidity_high'] = dev_dict.get('humidity_high', False) - self.details['water_tank_lifted'] = dev_dict.get( - 'water_tank_lifted', False) - self.details['display'] = dev_dict.get('display', False) - self.details['automatic_stop_reach_target'] = dev_dict.get( - 'automatic_stop_reach_target', True - ) - - def build_config_dict(self, conf_dict): - """Build configuration dict for 300s humidifier.""" - self.config['auto_target_humidity'] = conf_dict.get( - 'auto_target_humidity', 0) - self.config['display'] = conf_dict.get('display', False) - self.config['automatic_stop'] = conf_dict.get('automatic_stop', True) - - def get_details(self) -> None: - """Build 200S Humidifier details dictionary.""" - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body['cid'] = self.cid - body['configModule'] = self.config_module - body['payload'] = { - 'method': 'getHumidifierStatus', - 'source': 'APP', - 'data': {} - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - outer_result = r.get('result', {}) - inner_result = None - - if outer_result is not None: - 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_humid_dict(inner_result) - else: - logger.debug('error in inner result dict from humidifier') - if inner_result.get('configuration', {}): - self.build_config_dict(inner_result.get('configuration', {})) - else: - logger.debug('No configuration found in humidifier status') - else: - logger.debug('Error in humidifier response') - - def update(self): - """Update 200S Humidifier details.""" - self.get_details() - - def toggle_switch(self, toggle: bool) -> bool: - """Toggle humidifier on/off.""" - if not isinstance(toggle, bool): - logger.debug('Invalid toggle value for humidifier switch') - return False - - head = Helpers.bypass_header() - body = Helpers.bypass_body_v2(self.manager) - body['cid'] = self.cid - body['configModule'] = self.config_module - body['payload'] = { - 'data': { - 'enabled': toggle, - 'id': 0 - }, - 'method': 'setSwitch', - 'source': 'APP' - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug("Error toggling 200S humidifier - %s", self.device_name) - return False - - def turn_on(self) -> bool: - """Turn 200S Humidifier on.""" - return self.toggle_switch(True) - - def turn_off(self): - """Turn 200S Humidifier off.""" - return self.toggle_switch(False) - - def automatic_stop_on(self) -> bool: - """Turn 200S Humidifier automatic stop on.""" - return self.set_automatic_stop(True) - - def automatic_stop_off(self) -> bool: - """Turn 200S Humidifier automatic stop on.""" - return self.set_automatic_stop(False) - - def set_automatic_stop(self, mode: bool) -> bool: - """Set 200S Humidifier to automatic stop.""" - if mode not in (True, False): - logger.debug( - 'Invalid mode passed to set_automatic_stop - %s', mode) - return False - - head, body = self.__build_api_dict('setAutomaticStop') - if not head and not body: - return False - - body['payload']['data'] = { - 'enabled': mode - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - if isinstance(r, dict): - logger.debug('Error toggling automatic stop') - else: - logger.debug('Error in api return json for %s', self.device_name) - return False - - def set_display(self, mode: bool) -> bool: - """Toggle display on/off.""" - if not isinstance(mode, bool): - logger.debug("Mode must be True or False") - return False - - head, body = self.__build_api_dict('setDisplay') - - body['payload']['data'] = { - 'state': mode - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug("Error toggling 200S display - %s", self.device_name) - return False - - def turn_on_display(self) -> bool: - """Turn 200S Humidifier on.""" - return self.set_display(True) - - def turn_off_display(self): - """Turn 200S Humidifier off.""" - return self.set_display(False) - - def set_humidity(self, humidity: int) -> bool: - """Set target 200S Humidifier humidity.""" - if humidity < 30 or humidity > 80: - logger.debug("Humidity value must be set between 30 and 80") - return False - head, body = self.__build_api_dict('setTargetHumidity') - - if not head and not body: - return False - - body['payload']['data'] = { - 'target_humidity': humidity - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug('Error setting humidity') - return False - - def set_humidity_mode(self, mode: str) -> bool: - """Set humidifier mode - sleep or auto.""" - if mode.lower() not in ['sleep', 'auto']: - logger.debug('Invalid humidity mode used (sleep or auto)- %s', - mode) - return False - head, body = self.__build_api_dict('setHumidityMode') - if not head and not body: - return False - body['payload']['data'] = { - 'mode': mode.lower() - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug('Error setting humidity mode') - return False - - def set_mist_level(self, level: int) -> bool: - """Set humidifier mist level with int between 0 - 9.""" - if level < 1 or level > 9: - logger.debug('Humidifier mist level must be between 0 and 9') - return False - - head, body = self.__build_api_dict('setVirtualLevel') - if not head and not body: - return False - - body['payload']['data'] = { - 'id': 0, - 'level': level, - 'type': 'mist' - } - - r, _ = Helpers.call_api( - '/cloud/v2/deviceManaged/bypassV2', - method='post', - headers=head, - json=body, - ) - - if Helpers.code_check(r): - return True - logger.debug('Error setting mist level') - return False - - def display(self) -> None: - """Return formatted device info to stdout.""" - super().display() - disp1 = [ - ('Mode: ', self.details['mode'], ''), - ('Humidity: ', self.details['humidity'], 'percent'), - ('Mist Virtual Level: ', self.details['mist_virtual_level'], ''), - ('Mist Level: ', self.details['mist_level'], ''), - ('Water Lacks: ', self.details['water_lacks'], ''), - ('Humidity High: ', self.details['humidity_high'], ''), - ('Water Tank Lifted: ', self.details['water_tank_lifted'], ''), - ('Display: ', self.details['display'], ''), - ('Automatic Stop Reach Target: ', - self.details['automatic_stop_reach_target'], ''), - ('Auto Target Humidity: ', - self.config['auto_target_humidity'], 'percent'), - ('Automatic Stop: ', self.config['automatic_stop'], ''), - ] - for line in disp1: - print(f'{line[0]:.<29} {line[1]} {line[2]}') - - def displayJSON(self) -> str: - """Return humidifier status and properties in JSON output.""" - sup = super().displayJSON() - sup_val = json.loads(sup) sup_val.update( { 'Mode': self.details['mode'], @@ -1777,4 +1446,7 @@ def displayJSON(self) -> str: 'Automatic Stop': self.config['automatic_stop'], } ) + if self.night_light: + sup_val['Night Light Brightness'] = self.details[ + 'night_light_brightness'] return json.dumps(sup_val) From 884c9691f61531f7fba01eefdf61c39597e08539 Mon Sep 17 00:00:00 2001 From: Joe Trabulsy Date: Wed, 17 Nov 2021 22:24:00 -0500 Subject: [PATCH 3/4] Update azure-pipelines.yml --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6d3341d..7c996d2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,6 +14,8 @@ pr: jobs: - job: 'Validation' + pool: + vmImage: 'ubuntu-20.04' steps: - task: UsePythonVersion@0 inputs: From c80bfbc47a7d72a464a4791a8e95fce48886c154 Mon Sep 17 00:00:00 2001 From: Ben Hamilton Date: Sat, 20 Nov 2021 12:19:35 -0700 Subject: [PATCH 4/4] Fix 200S deviceType typo --- src/pyvesync/vesyncfan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyvesync/vesyncfan.py b/src/pyvesync/vesyncfan.py index d95d5bd..aa81e4a 100644 --- a/src/pyvesync/vesyncfan.py +++ b/src/pyvesync/vesyncfan.py @@ -1067,10 +1067,10 @@ class VeSyncHumid200300S(VeSyncBaseDevice): """200S/300S Humidifier Class.""" def __init__(self, details, manager): - """Initilize 200S/300S Humidifier class.""" + """Initialize 200S/300S Humidifier class.""" super().__init__(details, manager) self.enabled = True - if 'nightlight' in air_features.get(details.type): + if 'nightlight' in air_features.get(details['deviceType']): self.night_light = True else: self.night_light = False