pax_global_header00006660000000000000000000000064147544150330014520gustar00rootroot0000000000000052 comment=7d06a1a91f462ed931adfee5ed3000862e0f613d pyvesync-2.1.18/000077500000000000000000000000001475441503300134715ustar00rootroot00000000000000pyvesync-2.1.18/.coveragerc000066400000000000000000000013201475441503300156060ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] branch = True omit = *tests* [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ def __str__ def __eq__ def __hash__ if self\.debug # Disable test coverage for firmware updates def firmware_update def get_config # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain about abstract methods, they aren't run: @(abc\.)?abstractmethod ignore_errors = True [html] directory = coverage_html_reportpyvesync-2.1.18/.gitignore000066400000000000000000000006211475441503300154600ustar00rootroot00000000000000*.DS_Store build/* .mypy_cache build dist/* src/pyvesync.egg-info/* *.vscode **/*.pyc .coverage htmlcov/ .pytest_cache *__pycache__ cov_html/* cov_html/ test/* test/ /venv/ .idea/ src/tests/test1.py /src/tests/junit/ /.tox/ .tox/.tmp/ .tox/dist/ .tox/log/ tools/* tools/ test.py *.und tools/__init__.py tools/vesyncdevice.py pyvesync.und .venv mkdocs.yml requirements-docs.txt docs/ site/ overrides/pyvesync-2.1.18/.pylintrc000066400000000000000000000033701475441503300153410ustar00rootroot00000000000000[MASTER] ignore=src/tests, tools/ docs/ site/ load-plugins= pylint.extensions.docparams, pylint.extensions.mccabe [BASIC] good-names=i,j,k,x,r,e,v,_,b,dt,d [MESSAGES CONTROL] # Reasons disabled: # format - handled by black # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation # redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise # not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 # unnecessary-pass - readability for functions which only contain pass disable= format, abstract-method, arguments-differ, broad-exception-caught, # FIXME cyclic-import, duplicate-code, global-statement, inconsistent-return-statements, invalid-name, locally-disabled, not-an-iterable, too-few-public-methods, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-positional-arguments, too-many-public-methods, too-many-return-statements, too-many-statements, too-complex, # FIXME wildcard-import, unused-wildcard-import, unnecessary-pass, unused-argument, useless-super-delegation, [REPORTS] #reports=no [TYPECHECK] [FORMAT] expected-line-ending-format=LF max-line-length=90 pyvesync-2.1.18/CONTRIBUTING.md000066400000000000000000000056301475441503300157260ustar00rootroot00000000000000# Contributing to the pyvesync Library ## Setting up the Development Environment 1. Git clone the repository ```bash git clone https://github.com/webdjoe/pyvesync && cd pyvesync ``` 2. Create and activate a separate python virtual environment for pyvesync ```bash # Check Python version is 3.8 or higher python3 --version # or python --version or python3.8 --version # Create a new venv python3 -m venv pyvesync-venv # Activate the venv source pyvesync-venv/bin/activate # or .... pyvesync-venv\Scripts\activate.ps1 # on powershell pyvesync-venv\Scripts\activate.bat # on command prompt # Install development tools pip install -e .[dev] ``` 3. Make changes and test in virtual environment If the above steps were executed successfully, you should now have: - Code directory `pyvesync`(which we cloned from github) - Python venv directory `pyvesync` (all the dependencies/libraries are contained here) Any change in the code will now be directly reflected and can be tested. To deactivate the python venv, simply run `deactivate`. ## Testing Python with Tox Install tox, navigate to the pyvesync repository which contains the tox.ini file, and run tox as follows: ```bash # Run all tests and linters tox # Run tests, linters separately tox -e testenv # pytest tox -e pylint # linting tox -e lint # flake8 & pydocstrings tox -e mypy # type checkings ``` Tests are run based off of the API calls recorded in the [api](src/tests/api) directory. Please read the [Test Readme](src/tests/README.md) for further details on the structure of the tests. # Ensure new devices are Integrated in Tests If you integrate a new device, please read the [testing README](src/tests/README.md) to ensure that your device is tested. ## Testing with pytest and Writing API to YAML Part of the pytest test are against a library of API calls written to YAML files in the `tests` directory. If you are developing a new device, be aware that these tests will fail at first until you are ready to write the final API. There are two pytest command line arguments built into the tests to specify when to write the api data to YAML files or when to overwrite the existing API calls in the YAML files. To run a tests for development on existing devices or if you are not ready to write the api calls yet: ```bash # Through pytest pytest # or through tox tox -e testenv # you can also use the environments lint, pylint, mypy ``` If developing a new device and it is completed and thoroughly tested, pass the `--write_api` to pytest. Be sure to include the `--` before the argument in the tox command. ```bash pytest --write_api tox -e testenv -- --write_api ``` If fixing an existing device where the API call was incorrect or the api has changed, pass `--write_api` and `overwrite` to pytest. Both arguments need to be provided to overwrite existing API data already in the YAML files. ```bash pytest --write_api --overwrite tox -e testenv -- --write_api --overwrite ```pyvesync-2.1.18/LICENSE000066400000000000000000000020541475441503300144770ustar00rootroot00000000000000MIT License Copyright (c) 2019 Mark Perdue Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyvesync-2.1.18/MANIFEST.in000077500000000000000000000001021475441503300152230ustar00rootroot00000000000000include README.md LICENSE requirements.txt exclude test prune testpyvesync-2.1.18/README.md000066400000000000000000001040241475441503300147510ustar00rootroot00000000000000# pyvesync [![build status](https://img.shields.io/pypi/v/pyvesync.svg)](https://pypi.python.org/pypi/pyvesync) [![Build Status](https://dev.azure.com/webdjoe/pyvesync/_apis/build/status/webdjoe.pyvesync?branchName=master)](https://dev.azure.com/webdjoe/pyvesync/_build/latest?definitionId=4&branchName=master) [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/) [![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/) pyvesync is a library to manage VeSync compatible [smart home devices](#supported-devices) ## Table of Contents - [Installation](#installation) - [Supported Devices](#supported-devices) - [Etekcity Outlets](#etekcity-outlets) - [Wall Switches](#wall-switches) - [Levoit Air Purifiers](#levoit-air-purifiers) - [Etekcity Bulbs](#etekcity-bulbs) - [Valceno Bulbs](#valceno-bulbs) - [Levoit Humidifiers](#levoit-humidifiers) - [Usage](#usage) - [Configuration](#configuration) - [Time Zones](#time-zones) - [Outlet energy data update interval](#outlet-energy-data-update-interval) - [Example Usage](#example-usage) - [Get electricity metrics of outlets](#get-electricity-metrics-of-outlets) - [API Details](#api-details) - [Manager API](#manager-api) - [Standard Device API](#standard-device-api) - [Standard Properties](#standard-properties) - [Standard Methods](#standard-methods) - [Outlet API Methods \& Properties](#outlet-api-methods--properties) - [Outlet power and energy API Methods \& Properties](#outlet-power-and-energy-api-methods--properties) - [Model ESW15-USA 15A/1800W Methods (Have a night light)](#model-esw15-usa-15a1800w-methods-have-a-night-light) - [Standard Air Purifier Properties \& Methods](#standard-air-purifier-properties--methods) - [Air Purifier Properties](#air-purifier-properties) - [Air Purifier Methods](#air-purifier-methods) - [Levoit Purifier Core200S/300S/400S and Vital 100S/200S Properties](#levoit-purifier-core200s300s400s-vital-100s200s--everest-air-properties) - [Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Methods](#levoit-purifier-core200s300s400s-vital-100s200s--everest-air-methods) - [Levoit Vital 100S/200S Properties and Methods](#levoit-vital-100s200s--everest-air-properties-and-methods) - [Lights API Methods \& Properties](#lights-api-methods--properties) - [Brightness Light Bulb Method and Properties](#brightness-light-bulb-method-and-properties) - [Light Bulb Color Temperature Methods and Properties](#light-bulb-color-temperature-methods-and-properties) - [Multicolor Light Bulb Methods and Properties](#multicolor-light-bulb-methods-and-properties) - [Dimmable Switch Methods and Properties](#dimmable-switch-methods-and-properties) - [Levoit Humidifier Methods and Properties](#levoit-humidifier-methods-and-properties) - [Humidifier Properties](#humidifier-properties) - [Humidifer Methods](#humidifer-methods) - [600S warm mist feature](#600s-warm-mist-feature) - [Cosori Devices](#cosori-devices) - [Cosori 3.7 and 5.8 Quart Air Fryer](#cosori-37-and-58-quart-air-fryer) - [Air Fryer Properties](#air-fryer-properties) - [Air Fryer Methods](#air-fryer-methods) - [Timer DataClass](#timer-dataclass) - [JSON Output API](#json-output-api) - [JSON Output for All Devices](#json-output-for-all-devices) - [JSON Output for Outlets](#json-output-for-outlets) - [JSON Output for Dimmable Switch](#json-output-for-dimmable-switch) - [JSON Output for Dimmable Bulb](#json-output-for-dimmable-bulb) - [JSON Output for Tunable Bulb](#json-output-for-tunable-bulb) - [JSON Output for Multicolor Bulb](#json-output-for-multicolor-bulb) - [JSON Output for Air Purifier](#json-output-for-air-purifier) - [JSON Output for Core200S Purifier](#json-output-for-core200s-purifier) - [JSON Output for 400S Purifier](#json-output-for-400s-purifier) - [JSON Output for 600S Purifier](#json-output-for-600s-purifier) - [Notes](#notes) - [Debug mode](#debug-mode) - [Redact mode](#redact-mode) - [Feature Requests](#feature-requests) ## Installation Install the latest version from pip: ```bash pip install pyvesync ``` ## Supported Devices ### Etekcity Outlets 1. Voltson Smart WiFi Outlet- Round (7A model ESW01-USA) 2. Voltson Smart WiFi Outlet - Round (10A model ESW01-EU) 3. Voltson Smart Wifi Outlet - Round (10A model ESW03-USA) 4. Voltson Smart WiFi Outlet - Rectangle (15A model ESW15-USA) 5. Two Plug Outdoor Outlet (ESO15-TB) (Each plug is a separate `VeSyncOutlet` object, energy readings are for both plugs combined) ### Wall Switches 1. Etekcity Smart WiFi Light Switch (model ESWL01) 2. Etekcity Wifi Dimmer Switch (ESD16) ### Levoit Air Purifiers 1. LV-PUR131S 2. Core 200S 3. Core 300S 4. Core 400S 5. Core 600S 6. Vital 100S 7. Vital 200S 8. Everest Air ### Etekcity Bulbs 1. Soft White Dimmable Smart Bulb (ESL100) 2. Cool to Soft White Tunable Dimmable Bulb (ESL100CW) ### Valceno Bulbs 1. Valceno Multicolor Bulb (XYD0001) ### Levoit Humidifiers 1. Dual 200S 2. Classic 300S 3. LV600S 4. OasisMist 450S 5. OasisMist 600S 6. OasisMist 1000S ### Cosori Air Fryer 1. Cosori 3.7 and 5.8 Quart Air Fryer ### Fans 1. 42 in. Tower Fan ## Usage To start with the module: ```python from pyvesync import VeSync manager = VeSync("EMAIL", "PASSWORD", "TIME_ZONE", debug=False, redact=True) manager.login() # debug and redact are optional arguments, the above values are their defaults # Get/Update Devices from server - populate device lists manager.update() my_switch = manager.outlets[0] # Turn on the first switch my_switch.turn_on() # Turn off the first switch my_switch.turn_off() # Get energy usage data for outlets manager.update_energy() # Set bulb brightness to 75% of first bulb in the list my_bulb = manager.bulbs[0] my_bulb.set_brightness(75) # get its details in JSON and print print(my_bulb.displayJSON()) ``` Devices are stored in the respective lists in the instantiated `VeSync` class: ```python manager.login() manager.update() manager.outlets = [VeSyncOutletObjects] manager.switches = [VeSyncSwitchObjects] manager.fans = [VeSyncFanObjects] manager.bulbs = [VeSyncBulbObjects] # Get device (outlet, etc.) by device name dev_name = "My Device" for device in manager.outlets: if device.device_name == dev_name: my_device = device device.display() # Turn on switch by switch name switch_name = "My Switch" for switch in manager.switches: if switch.device_name == switch_name: switch.turn_on() ``` ## Configuration ### Time Zones The `time_zone` argument is optional but the specified time zone must match time zone in the tz database (IANNA Time Zone Database), see this link for reference: [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). The time zone determines how the energy history is generated for the smart outlets, i.e. for the week starts at 12:01AM Sunday morning at the specified time zone. If no time zone or an invalid time zone is entered the default is America/New_York ### Outlet energy data update interval If outlets are going to be continuously polled, a custom energy update interval can be set - The default is 6 hours (21600 seconds) ```python manager.energy_update_interval = 360 # time in seconds ``` ## Example Usage ### Get electricity metrics of outlets Bypass the interval check to trigger outlet energy update. ```python for s in manager.outlets: s.update_energy(check_bypass=False) # Get energy history for each device ``` ## API Details ### Manager API `VeSync.get_devices()` - Returns a list of devices `VeSync.login()` - Uses class username and password to login to VeSync `VeSync.update()` - Fetch updated information about devices `VeSync.update_all_devices()` - Fetch details for all devices (run `VeSyncDevice.update()`) `VeSync.update_energy(bypass_check=False)` - Get energy history for all outlets - Builds week, month and year nested energy dictionary. Set `bypass_check=True` to disable the library from checking the update interval ### Standard Device API These properties and methods are available for all devices. #### Standard Properties `VeSyncDevice.device_name` - Name given when registering device `VeSyncDevice.device_type` - Model of device, used to determine proper device class. `VeSyncDevice.connection_status` - Device online/offline `VeSyncDevice.config_module` - Special configuration identifier for device. Currently, not used in this API. `VeSyncDevice.sub_device_no` - Sub-device number for certain devices. Used for the outdoor outlet. `VeSyncDevice.device_status` - Device on/off `VeSyncDevice.is_on` - Returns boolean True/False if device is on. `VeSyncDevice.firmware_update` - Returns True is new firmware is available #### Standard Methods `VeSyncDevice.turn_on()` - Turn on the device `VeSyncDevice.turn_off()` - Turn off the device `VeSyncDevice.update()` - Fetch updated information about device `VeSyncDevice.active_time` - Return active time of the device in minutes `VeSyncDevice.get_config()` - Retrieve Configuration data such as firmware version for device and store in the `VeSyncDevice.config` dictionary ### Outlet API Methods & Properties #### Outlet power and energy API Methods & Properties `VeSyncOutlet.update_energy(bypass_check=False)` - Get outlet energy history - Builds week, month and year nested energy dictionary. Set `bypass_check=True` to disable the library from checking the update interval `VeSyncOutlet.energy_today` - Return current energy usage in kWh `VeSyncOutlet.power` - Return current power in watts of the device `VeSyncOutlet.voltage` - Return current voltage reading `VesyncOutlet.weekly_energy_total` - Return total energy reading for the past week in kWh, starts 12:01AM Sunday morning `VesyncOutlet.monthly_energy_total` - Return total energy reading for the past month in kWh `VesyncOutlet.yearly_energy_total` - Return total energy reading for the past year in kWh #### Model ESW15-USA 15A/1800W Methods (Have a night light) The rectangular smart switch model supports some additional functionality on top of the regular api call `VeSyncOutlet.nightlight_status` - Get the status of the nightlight `VeSyncOutlet.nightlight_brightness` - Get the brightness of the nightlight `VeSyncOutlet.turn_on_nightlight()` - Turn on the nightlight `VeSyncOutlet.turn_off_nightlight()` - Turn off the nightlight ### Standard Air Purifier Properties & Methods #### Air Purifier Properties `VeSyncFan.details` - Dictionary of device details ```python VeSyncFan.update() VeSyncFan.details = { 'active_time': 30004, # minutes 'filter_life': 45, # percent of filter life remaining 'screen_status': 'on', # display on/off 'level': 3, # fan level 'air_quality': 2, # air quality level } ``` NOTE: LV-PUR131S outputs `air_quality` as a string, such as `Excellent` `VeSyncFan.features` - Unique features to air purifier model. Currently, the only feature is air_quality, which is not supported on Core 200S. `VeSyncFan.modes` - Modes of operation supported by model - [sleep, off, auto] `VeSyncFan.fan_level` - Return the level of the fan `VeSyncFan.filter_life` - Return the percentage of filter life remaining `VeSyncFan.air_quality` - Return air quality level as integer, 1 being the best - Not available on Core 200S `VeSyncFan.air_quality_value` - PM2.5 air quality reading `VeSyncFan.screen_status` - Get Status of screen on/off #### Air Purifier Methods `VeSyncFan.auto_mode()` - Change mode to auto `VeSyncFan.manual_mode()` - Change fan mode to manual with fan level 1 `VeSyncFan.sleep_mode()` - Change fan mode to sleep `VeSyncFan.change_fan_speed(speed=None)` - Change fan speed. Call without speed to toggle to next speed Compatible levels for each model: - Core 200S [1, 2, 3] - Core 300S/400S [1, 2, 3, 4] - PUR131S [1, 2, 3] - Vital 100S/200S [1, 2, 3, 4] #### Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Properties `VeSyncFan.child_lock` - Return the state of the child lock (True=On/False=off) `VeSyncAir.night_light` - Return the state of the night light (on/dim/off) **Not available on Vital 100S/200S** #### Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Methods `VeSyncFan.child_lock_on()` Enable child lock `VeSyncFan.child_lock_off()` Disable child lock `VeSyncFan.turn_on_display()` Turn display on `VeSyncFan.turn_off_display()` Turn display off `VeSyncFan.set_night_light('on'|'dim'|'off')` - Set night light brightness `VeSyncFan.get_timer()` - Get any running timers, stores Timer DataClass in `VeSyncFan.timer`. See [Timer Dataclass](#timer-dataclass) `VeSyncFan.set_timer(timer_duration=3000)` - Set a timer for the device, only turns device off. Timer DataClass stored in `VeSyncFan.timer` `VeSyncFan.clear_timer()` - Cancel any running timer `VeSyncFan.reset_filter()` - Reset filter to 100% **NOTE: Only available on Core200S** #### Levoit Vital 100S/200S & Everest Air Properties and Methods The Levoit Vital 100S/200S has additional features not available on other models. `VeSyncFan.set_fan_speed` - The manual fan speed that is currently set (remains constant when in auto/sleep mode) `VeSyncFan.fan_level` - Returns the fan level purifier is currently operating on whether in auto/manual/sleep mode `VeSyncFan.light_detection` - Returns the state of the light detection mode (True=On/False=off) `VeSyncFan.light_detection_state` - Returns whether light is detected (True/False) `VeSyncFan.pet_mode()` - Set pet mode **NOTE: Only available on Vital 200S** `VeSyncFan.set_auto_preference(preference='default', room_size=600)` - Set the auto mode preference. Preference can be 'default', 'efficient' or quiet. Room size defaults to 600 `VeSyncFan.set_light_detection_on()` - Turn on light detection mode `VeSyncFan.set_light_detection_off()` - Turn off light detection mode #### Levoit Everest Air Properties & Methods `VeSyncFan.turbo_mode()` - Set turbo mode Additional properties in the `VeSyncFan['details']` dictionary: ```python VeSyncFan['Details'] = { 'pm1': 0, # air quality reading of particulates 1.0 microns 'pm10': 10, # air quality reading of particulates 10 microns 'fan_rotate_angle': 45, # angle of fan vents 'aq_percent': 45, # Air Quality percentage reading 'filter_open_state': False # returns bool of filter open } ``` ### Lights API Methods & Properties #### Brightness Light Bulb Method and Properties *Compatible with all bulbs* `VeSyncBulb.brightness` - Return brightness in percentage (1 - 100) `VeSyncBulb.set_brightness(brightness)` - Set bulb brightness values from 1 - 100 #### Light Bulb Color Temperature Methods and Properties **NOTE: only compatible with ESL100CW and Valceno Bulbs, NOT compatible with ESL100MC Bulbs** `VeSyncBulb.color_temp_pct` - Return color temperature in percentage (0 - 100) `VeSyncBulb.color_temp_kelvin` - Return brightness in Kelvin `VeSyncBulb.set_color_temp(color_temp)` - Set white temperature in percentage (0 - 100) #### Multicolor Light Bulb Methods and Properties *Compatible with ESL100MC & Valceno Bulbs* **Properties** `VeSyncBulb.color` - Returns a Dataclass with HSV and RGB attributes that are named tuples ```python VeSyncBulb.color.rbg = namedtuple('RGB', ['red', 'green', 'blue']) VeSyncBulb.color.hsv = namedtuple('HSV', ['hue', 'saturation', 'value']) ``` `VeSyncBulb.color_hsv` - Returns a named tuple with HSV values `VeSyncBulb.color_rgb` - Returns a named tuple with RGB values `VeSyncBulb.color_mode` - Return bulb color mode (string values: 'white', 'color', 'hsv') `VeSyncBulb.color_hue` - Return color hue (float values from 0.0 - 360.0) `VeSyncBulb.color_saturation` - Return color saturation (float values from 0.0 - 100.0) `VeSyncBulb.color_value` - Return color value (int values from 0 - 100) *The following properties are also still available for backwards compatibility* `VeSyncBulb.color_value_hsv` - Return color value in HSV named tuple format (hue: float 0.0-360.0, saturation: float 0.0-100.0, value: float 0-100 ) `VeSyncBulb.color_value_rgb` - Return color value in RGB named tuple format (red: float, green: float, blue: float 0-255.0) **Methods** `VeSyncBulb.set_hsv(hue, saturation, value)` - Set bulb color in HSV format - Arguments: hue (numeric) 0 - 360, saturation (numeric) 0-100, value (numeric) 0-100 - Returns bool `VeSyncBulb.set_rgb(red, green, blue)` - Set bulb color in RGB format - Arguments: red (numeric) 0-255, green (numeric) 0-255, blue (numeric) 0-255 - Returns bool `VeSyncBulb.enable_white_mode()` - Turn bulb to white mode - returns bool `VeSyncBulb.set_color_temp(color_temp)` - Set bulb white temperature (int values from 0 - 100) - Setting this will automatically force the bulb into White mode `VeSyncBulb.set_status(brightness, color_temp, color_saturation, color_hue, color_mode color_value)` - Set every property, in a single call - All parameters are optional **NOTE: Due to the varying API between bulbs, avoid setting the `color_mode` argument directly, instead set colors values with `set_hsv` or `set_rgb` to turn on color and use `enable_white_mode()` to turn off color.** #### Dimmable Switch Methods and Properties `VeSyncSwitch.brightness` - Return brightness of switch in percentage (1 - 100) `VeSyncSwitch.indicator_light_status` - return status of indicator light on switch `VeSyncSwitch.rgb_light_status` - return status of rgb light on faceplate `VeSyncSwitch.rgb_light_value` - return dictionary of rgb light color (0 - 255) `VeSyncSwitch.set_brightness(brightness)` - Set brightness of switch (1 - 100) `VeSyncSwitch.indicator_light_on()` - Turn indicator light on `VeSyncSwitch.indicator_light_off()` - Turn indicator light off `VeSyncSwitch.rgb_color_on()` - Turn rgb light on `VeSyncSwitch.rgb_color_off()` - Turn rgb light off `VeSyncSwitch.rgb_color_set(red, green, blue)` - Set color of rgb light (0 - 255) ### Levoit Humidifier Methods and Properties #### Humidifier Properties The details dictionary contains all device status details ```python VeSyncHumid.details = { 'humidity': 80, # percent humidity in room 'mist_virtual_level': 0, # Level of mist output 1 - 9 'mist_level': 0, 'mode': 'manual', # auto, manual, sleep 'water_lacks': False, 'humidity_high': False, 'water_tank_lifted': False, 'display': False, 'automatic_stop_reach_target': False, 'night_light_brightness': 0 } ``` The configuration dictionary shows current settings ```python VeSyncHumid.config = { 'auto_target_humidity': 80, # percent humidity in room 'display': True, # Display on/off 'automatic_stop': False } ``` The LV600S has warm mist settings that show in the details dictionary in addition to the key/values above. ```python VeSyncLV600S.details = { 'warm_mist_enabled': True, 'warm_mist_level': 2 } ``` `VeSyncHumid.humidity` - current humidity level in room `VeSyncHumid.mist_level` - current mist level `VeSyncHumid.mode` - Mode of operation - sleep, off, auto/humidity `VeSyncHumid.water_lacks` - Returns True if water is low `VeSyncHumid.auto_humidity` - Target humidity for auto mode `VeSyncHumid.auto_enabled` - Returns true if auto stop enabled at target humidity #### Humidifer Methods `VeSyncHumid.automatic_stop_on()` Set humidifier to stop at set humidity `VeSyncHumid.automatic_stop_off` Set humidifier to run continuously `VeSyncHumid.turn_on_display()` Turn display on `VeSyncHumid.turn_off_display()` Turn display off `VeSyncHumid.set_humidity(30)` Set humidity between 30 and 80 percent `VeSyncHumid.set_night_light_brightness(50)` Set nightlight brightness between 1 and 100 `VeSyncHumid.set_humidity_mode('sleep')` Set humidity mode - sleep/auto `VeSyncHumid.set_mist_level(4)` Set mist output 1 - 9 #### 600S warm mist feature `VeSync600S.warm_mist_enabled` - Returns true if warm mist feature is enabled `VeSync600S.set_warm_level(2)` - Sets warm mist level ### Cosori Devices Cosori devices are found under the `manager.kitchen` VeSync class attribute. #### Cosori 3.7 and 5.8 Quart Air Fryer The Cosori 3.7 and 5.8 Quart Air Fryer has several methods and properties that can be used to monitor and control the device. This library splits the functionality and status into two classes that are both accessible from the device instance. To maintain consistency of state, the update() method is called after each of the methods that change the state of the device. There is also an instance attribute that can be set `VeSyncAirFryer158.refresh_interval` that will set the interval in seconds that the state of the air fryer should be updated before a method that changes state is called. This is an additional API call but is necessary to maintain state, especially when trying to `pause` or `resume` the device. Defaults to 60 seconds but can be set via: ```python # Change to 120 seconds before status is updated between calls VeSyncAirFryer158.refresh_interval = 120 # Set status update before every call VeSyncAirFryer158.refresh_interval = 0 # Disable status update before every call VeSyncAirFryer158.refresh_interval = -1 ``` ##### Air Fryer Properties All properties cannot be directly set, they must be set from the `get_details()` or methods that set the status. They can be set through the `VeSyncAirFryer158.fryer_status` dataclass but should be avoided. This separation of functionality and status is purposeful to avoid inconsistent states. `VeSyncAirFryer158.temp_unit` - Temperature units of the device (`fahrenheit` or `celsius`) `VeSyncAirFryer158.current_temp` - Current temperature in the defined temperature units. If device is not running, this defaults to `None` `VeSyncAirFryer158.cook_set_temp` - Set temperature or target temperature for preheat `VeSyncAirFryer158.cook_last_time` - The last minutes remaining returned from API for cook mode `VeSyncAirFryer158.preheat_last_time` - The last minutes remaining returned from API for preheat mode `VeSyncAirFryer158.cook_status` - Status of air fryer. This can be the following states: 1. `standby` - Air fryer is off and no cook or preheat is in progress 2. `cooking` - Air fryer is actively cooking 3. `cookStop` - Cooking is paused and can be resumed 4. `cookEnd` - Cooking is ended and can be resumed 5. `heating` - Air fryer is preheating 6. `preheatStop` - Preheat is paused and can be resumed 7. `heatEnd` - Preheat is ended and cooking mode can be started with `cook_from_preheat()` method `VeSyncAirFryer158.is_heating` - Returns true if air fryer is preheating `VeSyncAirFryer158.is_cooking` - Returns true if air fryer is cooking `VeSyncAirFryer158.is_paused` - Returns true if air fryer is paused and can be resumed `VeSyncAirFryer158.remaining_time` - Returns minutes remaining based on timestamp of last API return when air fryer is running. Returns `None` if not running `VeSyncAirFryer158.fryer_status` - Dataclass that contains the status of the air fryer. The attributes of this Dataclass are directly accessible from the `VeSyncAirFryer158` properties and **should not be directly set.** ##### Air Fryer Methods `VeSyncAirFryer158.update()` - Retrieve current status `VeSyncAirFryer158.cook(set_temp: int, set_time: int)` - Set air fryer cook mode based on time and temp in defined units `VeSyncAirFryer158.set_preheat(target_temp: int, cook_time: int)` - Set air fryer preheat mode based on time and temp in defined units `VeSyncAirFryer158.cook_from_preheat()` - Start cook mode when air fryer is in `preheatEnd` state `VeSyncAirFryer158.pause()` - Pause air fryer when in `cooking` or `heating` state `VeSyncAirFryer158.resume()` - Resume air fryer when in `cookStop` or `preheatStop` state `VeSyncAirFryer158.end()` - End cooking or preheating and return air fryer to `standby` state ### Timer DataClass This is the a Timer DataClass that is used in the `get_timer()` or `set_timer()` methods *only implemented for Levoit Core 200S and 300S Air Purifier*, will eventually integrate with remaining devices. This object is created when the device timer methods are called. **The `pause()`, `resume()` and `stop()` methods for this DataClass only impact the timer locally and do not update the API.** ```python from pyvesync.helpers import Timer timer = Timer(timer_duration=60, id=1) # Get time remaining in seconds # Calculates based on timer elapsed each time property is called timer.remaining_time # Get status timer.status # Get action timer.action # Set status - active or done timer.status = 'active' # set time remaining in seconds, does not edit status timer.remaining_time = 120 # Pause timer - Does not update API - only pauses locally timer.pause() # End timer -Does not update API - only ends locally timer.end() # Resume timer - Does not update API - only Resumes locally timer.start() ``` ### JSON Output API The `device.displayJSON()` method outputs properties and status of the device #### JSON Output for All Devices ```python device.displayJSON() #Returns: { "Device Name": "Device 1", "Model": "Device Model", "Subdevice No": "1", "Status": "on", "Online": "online", "Type": "Device Type", "CID": "DEVICE-CID" } ``` #### JSON Output for Outlets ```python { "Active Time": "1", # in minutes "Energy": "2.4", # today's energy in kWh "Power": "12", # current power in W "Voltage": "120", # current voltage "Energy Week": "12", # totaly energy of week in kWh "Energy Month": "50", # total energy of month in kWh "Energy Year": "89", # total energy of year in kWh } ``` #### JSON Output for Dimmable Switch This output only applies to dimmable switch. The standard switch has the default device JSON output shown [above](#json-output-for-all-devices) ```python { "Indicator Light": "on", # status of indicator light "Brightness": "50", # percent brightness "RGB Light": "on" # status of RGB Light on faceplate } ``` #### JSON Output for Dimmable Bulb ```python # output for dimmable bulb { # all default outputs plus... "Brightness": "50" # brightness in percent } ``` #### JSON Output for Tunable Bulb ```python # output for tunable bulb { # all default outputs plus... "Brightness": "50" # brightness in percent "Kelvin": "5400" # white temperature in Kelvin } ``` #### JSON Output for Multicolor Bulb ```python # output for valceno multicolor bulb { # all default outputs plus... "Brightness": "100", # brightness in percent (also used for color value in hsv mode) "WhiteTemperaturePct": "0", # white temperature in percent "WhiteTemperatureKelvin": "2700", # white temperature in Kelvin "ColorHSV": "hsv(hue=79.99, saturation=90.0, value=100)", # color definition in HSV model "ColorRGB": "rgb(red=178.5, green=255.0, blue=25.5)", # color definition in RGB model "ColorMode": "hsv" # color mode ( white / hsv ) } ``` #### JSON Output for Air Purifier ```python { "Active Time": "50", # minutes "Fan Level": "2", # fan level 1-3 "Air Quality": "95", # air quality in percent "Mode": "auto", "Screen Status": "on", "Filter Life": "99" # remaining filter life in percent } ``` ```python { "Mode": "manual", # auto, manual, sleep "Humidity": 20, # percent "Mist Virtual Level": 6, # Mist level 1 - 9 "Water Lacks": true, # True/False "Water Tank Lifted": true, # True/False "Display": true, # True/False "Automatic Stop Reach Target": true, "Night Light Brightness": 10, # 1 - 100 "Auto Target Humidity": true, # True/False "Automatic Stop": true # True/False } ``` #### JSON Output for Core200S Purifier ```python { "Device Name": "MyPurifier", "Model": "Core200S", "Subdevice No": "None", "Status": "on", "Online": "online", "Type": "wifi-air", "CID": "asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55", "Mode": "manual", "Filter Life": "99", "Fan Level": "1", "Display": true, "Child Lock": false, "Night Light": "off", "Display Config": true, "Display_Forever Config": false } ``` #### JSON Output for 400S Purifier ```python { "Device Name": "MyPurifier", "Model": "Core200S", "Subdevice No": "None", "Status": "on", "Online": "online", "Type": "wifi-air", "CID": "", "Mode": "manual", "Filter Life": "100", "Air Quality Level": "5", "Air Quality Value": "1", "Fan Level": "1", "Display": true, "Child Lock": false, "Night Light": "off", "Display Config": true, "Display_Forever Config": false } ``` #### JSON Output for 600S Purifier ```python { "Device Name": "My 600s", "Model": "LAP-C601S-WUS", "Subdevice No": "None", "Status": "on", "Online": "online", "Type": "wifi-air", "CID": "", "Mode": "manual", "Filter Life": "98", "Air Quality Level": "5", "Air Quality Value": "1", "Fan Level": "3", "Display": true, "Child Lock": false, "Night Light": "off", "Display Config": true, "Display_Forever Config": false } ``` ## Notes More detailed data is available within the `VesyncOutlet` by inspecting the `VesyncOutlet.energy` dictionary. The `VesyncOutlet.energy` object includes 3 nested dictionaries `week`, `month`, and `year` that contain detailed weekly, monthly and yearly data ```python VesyncOutlet.energy['week']['energy_consumption_of_today'] VesyncOutlet.energy['week']['cost_per_kwh'] VesyncOutlet.energy['week']['max_energy'] VesyncOutlet.energy['week']['total_energy'] VesyncOutlet.energy['week']['data'] # which itself is a list of values ``` ## Debug mode To make it easier to debug, there is a `debug` argument in the `VeSync` method. This prints out your device list and any other debug log messages. ```python import pyvesync.vesync as vs manager = vs.VeSync('user', 'pass', debug=True) manager.login() manager.update() # Prints device list returned from Vesync ``` ## Redact mode To make it easier to post logs online , there is a `redact` argument in the `VeSync` method. This redacts any sensitive information from the logs. The dafault is set to True ```python import pyvesync.vesync as vs manager = vs.VeSync('user', 'pass', debug=True, redact=True) manager.login() manager.update() # Prints device list returned from Vesync ``` ## Feature Requests Before filing an issue to request a new feature or device, please ensure that you will take the time to test the feature throuroughly. New features cannot be simply tested on Home Assistant. A separate integration must be created which is not part of this library. In order to test a new feature, clone the branch and install into a new virtual environment. ```bash mkdir python_test && cd python_test # Check Python version is 3.8 or higher python3 --version # or python --version or python3.8 --version # Create a new venv python3 -m venv pyvesync-venv # Activate the venv on linux source pyvesync-venv/bin/activate # or .... pyvesync-venv\Scripts\activate.ps1 # on powershell pyvesync-venv\Scripts\activate.bat # on command prompt # Install branch to be tested into new virtual environment pip install git+https://github.com/webdjoe/pyvesync@BRANCHNAME ``` Test functionality with a script `test.py` ```python import sys import logging import json from pyvesync import VeSync logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) USERNAME = "YOUR USERNAME" PASSWORD = "YOUR PASSWORD" def test_device(): # Instantiate VeSync class and login manager = VeSync(USERNAME, PASSWORD, debug=True) if manager.login() == False: logger.debug("Unable to login") return # Pull and update devices manager.update() fan = None logger.debug(str(manager.fans)) for dev in manager.fans: # Print all device info logger.debug(dev.device_name + "\n") logger.debug(dev.display()) # Find correct device if dev.device_name.lower() == DEVICE_NAME.lower(): fan = dev break if fan == None: logger.debug("Device not found") logger.debug("Devices found - \n %s", str(manager._dev_list)) return logger.debug('--------------%s-----------------' % fan.device_name) logger.debug(dev.display()) logger.debug(dev.displayJSON()) # Test all device methods and functionality # Test Properties logger.debug("Fan is on - %s", fan.is_on) logger.debug("Modes - %s", fan.modes) logger.debug("Fan Level - %s", fan.fan_level) logger.debug("Fan Air Quality - %s", fan.air_quality) logger.debug("Screen Status - %s", fan.screen_status) fan.turn_on() fan.turn_off() fan.sleep_mode() fan.auto_mode() fan.manual_mode() fan.change_fan_speed(3) fan.change_fan_speed(2) fan.child_lock_on() fan.child_lock_off() fan.turn_off_display() fan.turn_on_display() fan.set_light_detection_on() logger.debug(fan.light_detection_state) logger.debug(fan.light_detection) # Only on Vital 200S fan.pet_mode() logger.debug("Set Fan Speed - %s", fan.set_fan_speed) logger.debug("Current Fan Level - %s", fan.fan_level) logger.debug("Current mode - %s", fan.mode) # Display all device info logger.debug(dev.display()) logger.debug(dev.displayJSON()) if __name__ == "__main__": logger.debug("Testing device") test_device() ... ``` ## Device Requests SSL pinning makes capturing packets much harder. In order to be able to capture packets, SSL pinning needs to be disabled before running an SSL proxy. Use an Android emulator such as Android Studio, which is available for Windows and Linux for free. Download the APK from APKPure or a similiar site and use [Objection](https://github.com/sensepost/objection) or [Frida](https://frida.re/docs/gadget/). Followed by capturing the packets with Charles Proxy or another SSL proxy application. Be sure to capture all packets from the device list and each of the possible device menus and actions. Please redact the `accountid` and `token` from the captured packets. If you feel you must redact other keys, please do not delete them entirely. Replace letters with "A" and numbers with "1", leave all punctuation intact and maintain length. For example: Before: ```json { "tk": "abc123abc123==3rf", "accountId": "123456789", "cid": "abcdef12-3gh-ij" } ``` After: ```json { "tk": "AAA111AAA111==1AA", "accountId": "111111111", "cid": "AAAAAA11-1AA-AA" } ``` ## Contributing All [contributions](CONTRIBUTING.md) are welcome. This project is licensed under [MIT](LICENSE). pyvesync-2.1.18/azure-pipelines.yml000066400000000000000000000031171475441503300173320ustar00rootroot00000000000000# Python package # Create and test a Python package on multiple Python versions. # Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/python trigger: branches: include: - '*' pr: - '*' jobs: - job: 'Validation' pool: vmImage: 'ubuntu-20.04' steps: - task: UsePythonVersion@0 inputs: versionSpec: '3.9' displayName: 'Use Python 3.9' - script: | python -m pip install --upgrade pip pip install -r requirements.txt displayName: 'Install dependencies' - script: | pip install flake8 flake8-docstrings flake8 src/pyvesync displayName: 'Flake8' - script: | pip install pylint pylint src/pyvesync displayName: 'Pylint' - script: | pip install mypy mypy src/pyvesync python3 -m pip install types-requests displayName: 'MyPy' - job: 'pytest' pool: vmImage: 'ubuntu-20.04' strategy: matrix: Python39: python.version: '3.9' Python310: python.version: '3.10' Python311: python.version: '3.11' steps: - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' displayName: 'Use Python $(python.version)' - script: | set -e python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-azurepipelines pytest-sugar pyyaml displayName: 'Install dependencies' - script: | pytest --junitxml=reports/pytest.xml displayName: 'Run Tests' pyvesync-2.1.18/mypy.ini000066400000000000000000000004711475441503300151720ustar00rootroot00000000000000[mypy] python_version=3.9 [mypy-src.pyvesync.vesyncfan.fan_modules] ignore_missing_imports = True [mypy-src.pyvesync.vesyncbulb.bulb_modules] ignore_missing_imports = True [pyvesync.vesyncoutlet.outlet_modules] ignore_missing_imports = True [pyvesync.vesyncswitch.switch_modules] ignore_missing_imports = Truepyvesync-2.1.18/requirements.txt000066400000000000000000000000201475441503300167450ustar00rootroot00000000000000requests>=2.20.0pyvesync-2.1.18/ruff.toml000066400000000000000000000035131475441503300153320ustar00rootroot00000000000000exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", "test", "tests", ] line-length = 90 indent-width = 4 [lint] select = ["ALL"] ignore = ["T201", # PRINT statement - IGNORE "COM812", # Missing trailing comma - IGNORE "TRY003", # Avoid specifying long messages outside the exception class - TODO: Add exception classes "TRY301", # raise within try - TODO: Fix this "BLE001", # Broad Exceptions are not allowed - TODO: Fix this "EM102", # Exception must not use an f-string literal, assign to variable first - TODO: add exception classes "I001", # Import sort - TODO: Fix this "EM101", # Exception must not use a string literal, assign to variable first - TODO: add exception classes "FBT001", # type hint positional argument - bool is not allowed - IGNORE "FBT003", # Bool positional argument - IGNORE "TD002", # Todo error - IGNORE "TD003", # Todo error - IGNORE "FIX002", # Fixme error - IGNORE ] # Todo: Add exception classes EM102, BLE001, EM101 # Todo: Fix import sorting issue I001 unfixable = ["B"] [lint.per-file-ignores] "vesync.py" = ["F403"] "vesyncbulb.py" = ["PLR2004"] [lint.pep8-naming] extend-ignore-names = ["displayJSON"] [lint.pydocstyle] convention = "google" [lint.pylint] max-public-methods = 30 max-args = 6 [format] quote-style = "preserve" indent-style = "space" skip-magic-trailing-comma = false line-ending = "lf" pyvesync-2.1.18/setup.cfg000077500000000000000000000004521475441503300153160ustar00rootroot00000000000000[bdist_wheel] universal = 1 [metadata] license_file = LICENSE [tool:pytest] testpaths = src/tests norecursedirs = .git pythonpath = src log_level = DEBUG log_cli_level = DEBUG log_cli = True [flake8] max-line-length = 90 extend-ignore = D102 [pycodestyle] max-line-length = 90 statistics = Truepyvesync-2.1.18/setup.py000066400000000000000000000026331475441503300152070ustar00rootroot00000000000000"""pyvesync setup script.""" from os import path from setuptools import setup, find_packages this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: long_description = f.read() setup( name='pyvesync', version='2.1.18', description='pyvesync is a library to manage Etekcity\ Devices, Cosori Air Fryers and Levoit Air \ Purifiers run on the VeSync app.', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/webdjoe/pyvesync', author='Mark Perdue, Joe Trabulsy', author_email='webdjoe@gmail.com', license='MIT', classifiers=[ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Natural Language :: English', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Home Automation', ], keywords=['iot', 'vesync', 'levoit', 'etekcity', 'cosori', 'valceno'], packages=find_packages('src', exclude=['tests', 'test*']), package_dir={'': 'src'}, install_requires=['requests>=2.20.0'], extras_require={ 'dev': ['pytest', 'pytest-cov', 'pyyaml', 'tox'] }, python_requires='>=3.9', ) pyvesync-2.1.18/src/000077500000000000000000000000001475441503300142605ustar00rootroot00000000000000pyvesync-2.1.18/src/pyvesync/000077500000000000000000000000001475441503300161405ustar00rootroot00000000000000pyvesync-2.1.18/src/pyvesync/__init__.py000077500000000000000000000003151475441503300202530ustar00rootroot00000000000000"""VeSync API Library.""" # pylint: skip-file # flake8: noqa from .vesync import VeSync import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)5s - %(message)s' ) pyvesync-2.1.18/src/pyvesync/helpers.py000066400000000000000000000647361475441503300201740ustar00rootroot00000000000000"""Helper functions for VeSync API.""" from __future__ import annotations import hashlib import logging import time import json import colorsys from dataclasses import dataclass, field, InitVar from typing import Any, NamedTuple, Union, TYPE_CHECKING import re import requests if TYPE_CHECKING: from pyvesync.vesync import VeSync logger = logging.getLogger(__name__) API_BASE_URL = 'https://smartapi.vesync.com' API_RATE_LIMIT = 30 # If device is out of reach, the cloud api sends a timeout response after 7 seconds, # using 8 here so there is time enough to catch that message API_TIMEOUT = 8 USER_AGENT = ("VeSync/3.2.39 (com.etekcity.vesyncPlatform;" " build:5; iOS 15.5.0) Alamofire/5.2.1") DEFAULT_TZ = 'America/New_York' DEFAULT_REGION = 'US' APP_VERSION = '2.8.6' PHONE_BRAND = 'SM N9005' PHONE_OS = 'Android' MOBILE_ID = '1234567890123456' USER_TYPE = '1' BYPASS_APP_V = "VeSync 3.0.51" BYPASS_HEADER_UA = 'okhttp/3.12.1' NUMERIC = Union[int, float, str, None] REQUEST_T = dict[str, Any] class Helpers: """VeSync Helper Functions.""" @staticmethod def req_headers(manager: VeSync) -> dict[str, str]: """Build header for legacy api GET requests. Args: manager (VeSyncManager): Instance of VeSyncManager. Returns: dict: Header dictionary for api requests. Examples: >>> req_headers(manager) { 'accept-language': 'en', 'accountId': manager.account_id, 'appVersion': APP_VERSION, 'content-type': 'application/json', 'tk': manager.token, 'tz': manager.time_zone, } """ return { 'accept-language': 'en', 'accountId': manager.account_id, 'appVersion': APP_VERSION, 'content-type': 'application/json', 'tk': manager.token, 'tz': manager.time_zone, } @staticmethod def req_header_bypass() -> dict[str, str]: """Build header for api requests on 'bypass' endpoint. Returns: dict: Header dictionary for api requests. Examples: >>> req_header_bypass() { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': BYPASS_HEADER_UA, } """ return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': BYPASS_HEADER_UA, } @staticmethod def req_body_base(manager: VeSync) -> dict[str, str]: """Return universal keys for body of api requests. Args: manager (VeSyncManager): Instance of VeSyncManager. Returns: dict: Body dictionary for api requests. Examples: >>> req_body_base(manager) { 'timeZone': manager.time_zone, 'acceptLanguage': 'en', } """ return {'timeZone': manager.time_zone, 'acceptLanguage': 'en'} @staticmethod def req_body_auth(manager: VeSync) -> REQUEST_T: """Keys for authenticating api requests. Args: manager (VeSyncManager): Instance of VeSyncManager. Returns: dict: Authentication keys for api requests. Examples: >>> req_body_auth(manager) { 'accountID': manager.account_id, 'token': manager.token, } """ return {'accountID': manager.account_id, 'token': manager.token} @staticmethod def req_body_details() -> REQUEST_T: """Detail keys for api requests. Returns: dict: Detail keys for api requests. Examples: >>> req_body_details() { 'appVersion': APP_VERSION, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'traceId': str(int(time.time())), } """ return { 'appVersion': APP_VERSION, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'traceId': str(int(time.time())), } @classmethod def req_body(cls, manager: VeSync, type_: str) -> REQUEST_T: # noqa: C901 """Builder for body of api requests. Args: manager (VeSyncManager): Instance of VeSyncManager. type_ (str): Type of request to build body for. Returns: dict: Body dictionary for api requests. Note: The body dictionary will be built based on the type of request. The type of requests include: - login - devicestatus - devicelist - devicedetail - energy_week - energy_month - energy_year - bypass - bypassV2 - bypass_config """ body: REQUEST_T = cls.req_body_base(manager) if type_ == 'login': body |= cls.req_body_details() body |= { 'email': manager.username, 'password': cls.hash_password(manager.password), 'devToken': '', 'userType': USER_TYPE, 'method': 'login' } return body body |= cls.req_body_auth(manager) if type_ == 'devicestatus': return body body |= cls.req_body_details() if type_ == 'devicelist': body['method'] = 'devices' body['pageNo'] = '1' body['pageSize'] = '100' elif type_ == 'devicedetail': body['method'] = 'devicedetail' body['mobileId'] = MOBILE_ID elif type_ == 'energy_week': body['method'] = 'energyweek' body['mobileId'] = MOBILE_ID elif type_ == 'energy_month': body['method'] = 'energymonth' body['mobileId'] = MOBILE_ID elif type_ == 'energy_year': body['method'] = 'energyyear' body['mobileId'] = MOBILE_ID elif type_ == 'bypass': body['method'] = 'bypass' elif type_ == 'bypassV2': body['deviceRegion'] = DEFAULT_REGION body['method'] = 'bypassV2' elif type_ == 'bypass_config': body['method'] = 'firmwareUpdateInfo' return body @staticmethod def calculate_hex(hex_string: str) -> float: """Credit for conversion to itsnotlupus/vesync_wsproxy.""" hex_conv = hex_string.split(':') return (int(hex_conv[0], 16) + int(hex_conv[1], 16)) / 8192 @staticmethod def hash_password(string: str) -> str: """Encode password.""" return hashlib.md5(string.encode('utf-8')).hexdigest() # noqa: S324 shouldredact = True @classmethod def redactor(cls, stringvalue: str) -> str: """Redact sensitive strings from debug output. This method searches for specific sensitive keys in the input string and replaces their values with '##_REDACTED_##'. The keys that are redacted include: - token - password - email - tk - accountId - authKey - uuid - cid Args: stringvalue (str): The input string potentially containing sensitive information. Returns: str: The redacted string with sensitive information replaced by '##_REDACTED_##'. """ if cls.shouldredact: stringvalue = re.sub( ( r"(?i)" r'((?<=token":\s")|' r'(?<=password":\s")|' r'(?<=email":\s")|' r'(?<=tk":\s")|' r'(?<=accountId":\s")|' r'(?<=authKey":\s")|' r'(?<=uuid":\s")|' r'(?<=cid":\s")|' r"(?<=token\s)|" r"(?<=account_id\s))" r'[^"\s]+' ), "##_REDACTED_##", stringvalue, ) return stringvalue @staticmethod def nested_code_check(response: dict) -> bool: """Return true if all code values are 0. Args: response (dict): API response. Returns: bool: True if all code values are 0, False otherwise. """ if isinstance(response, dict): for key, value in response.items(): if (key == 'code' and value != 0) or \ (isinstance(value, dict) and not Helpers.nested_code_check(value)): return False return True @staticmethod def call_api(api: str, method: str, json_object: dict | None = None, headers: dict | None = None) -> tuple: """Make API calls by passing endpoint, header and body. api argument is appended to https://smartapi.vesync.com url Args: api (str): Endpoint to call with https://smartapi.vesync.com. method (str): HTTP method to use. json_object (dict): JSON object to send in body. headers (dict): Headers to send with request. Returns: tuple: Response and status code. """ response = None status_code = None try: logger.debug("=======call_api=============================") logger.debug("[%s] calling '%s' api", method, api) logger.debug("API call URL: \n %s%s", API_BASE_URL, api) logger.debug("API call headers: \n %s", Helpers.redactor(json.dumps(headers, indent=2))) logger.debug("API call json: \n %s", Helpers.redactor(json.dumps(json_object, indent=2))) if method.lower() == 'get': r = requests.get( API_BASE_URL + api, json=json_object, headers=headers, timeout=API_TIMEOUT ) elif method.lower() == 'post': r = requests.post( API_BASE_URL + api, json=json_object, headers=headers, timeout=API_TIMEOUT ) elif method.lower() == 'put': r = requests.put( API_BASE_URL + api, json=json_object, headers=headers, timeout=API_TIMEOUT ) else: raise NameError(f'Invalid method {method}') except requests.exceptions.RequestException as e: logger.debug(e) except Exception as e: logger.debug(e) else: if r.status_code == 200: status_code = 200 if r.content: response = r.json() logger.debug("API response: \n\n %s \n ", Helpers.redactor(json.dumps(response, indent=2))) else: logger.debug('Unable to fetch %s%s', API_BASE_URL, api) return response, status_code @staticmethod def code_check(r: dict) -> bool: """Test if code == 0 for successful API call.""" if r is None: logger.error('No response from API') return False return (isinstance(r, dict) and r.get('code') == 0) @staticmethod def build_details_dict(r: dict) -> dict: """Build details dictionary from API response. Args: r (dict): API response. Returns: dict: Details dictionary. Examples: >>> build_details_dict(r) { 'active_time': 1234, 'energy': 168, 'night_light_status': 'on', 'night_light_brightness': 50, 'night_light_automode': 'on', 'power': 1, 'voltage': 120, } """ return { 'active_time': r.get('activeTime', 0), 'energy': r.get('energy', 0), 'night_light_status': r.get('nightLightStatus'), 'night_light_brightness': r.get('nightLightBrightness'), 'night_light_automode': r.get('nightLightAutomode'), 'power': r.get('power', 0), 'voltage': r.get('voltage', 0), } @staticmethod def build_energy_dict(r: dict) -> dict: """Build energy dictionary from API response. Note: For use with **Etekcity** outlets only Args: r (dict): API response. Returns: dict: Energy dictionary. """ return { 'energy_consumption_of_today': r.get( 'energyConsumptionOfToday', 0), 'cost_per_kwh': r.get('costPerKWH', 0), 'max_energy': r.get('maxEnergy', 0), 'total_energy': r.get('totalEnergy', 0), 'currency': r.get('currency', 0), 'data': r.get('data', 0), } @staticmethod def build_config_dict(r: dict) -> dict: """Build configuration dictionary from API response. Contains firmware version, max power, threshold, power protection status, and energy saving status. Note: Energy and power stats only available for **Etekcity** outlets. Args: r (dict): API response. Returns: dict: Configuration dictionary. Examples: >>> build_config_dict(r) { 'current_firmware_version': '1.2.3', 'latest_firmware_version': '1.2.4', 'max_power': 1000, 'threshold': 500, 'power_protection': 'on', 'energy_saving_status': 'on', } """ if r.get('threshold') is not None: threshold = r.get('threshold') else: threshold = r.get('threshHold') return { 'current_firmware_version': r.get('currentFirmVersion'), 'latest_firmware_version': r.get('latestFirmVersion'), 'maxPower': r.get('maxPower'), 'threshold': threshold, 'power_protection': r.get('powerProtectionStatus'), 'energy_saving_status': r.get('energySavingStatus'), } @classmethod def bypass_body_v2(cls, manager: VeSync) -> dict: """Build body dict for second version of bypass api calls. Args: manager (VeSyncManager): Instance of VeSyncManager. Returns: dict: Body dictionary for bypass api calls. Examples: >>> bypass_body_v2(manager) { 'timeZone': manager.time_zone, 'acceptLanguage': 'en', 'accountID': manager.account_id, 'token': manager.token, 'appVersion': APP_VERSION, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'traceId': str(int(time.time())), 'method': 'bypassV2', 'debugMode': False, 'deviceRegion': DEFAULT_REGION, } """ bdy: dict[str, str | bool] = {} bdy.update( **cls.req_body(manager, "bypass") ) bdy['method'] = 'bypassV2' bdy['debugMode'] = False bdy['deviceRegion'] = DEFAULT_REGION return bdy @staticmethod def bypass_header() -> dict: """Build bypass header dict. Returns: dict: Header dictionary for bypass api calls. Examples: >>> bypass_header() { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': BYPASS_HEADER_UA, } """ return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': 'okhttp/3.12.1', } @staticmethod def named_tuple_to_str(named_tuple: NamedTuple) -> str: """Convert named tuple to string. Args: named_tuple (namedtuple): Named tuple to convert to string. Returns: str: String representation of named tuple. Examples: >>> named_tuple_to_str(HSV(100, 50, 75)) 'hue: 100, saturation: 50, value: 75, ' """ tuple_str = '' for key, val in named_tuple._asdict().items(): tuple_str += f'{key}: {val}, ' return tuple_str class HSV(NamedTuple): """HSV color space named tuple. Used as an attribute in the `pyvesync.helpers.Color` dataclass. Attributes: hue (float): The hue component of the color, typically in the range [0, 360). saturation (float): The saturation component of the color, typically in the range [0, 1]. value (float): The value (brightness) component of the color, typically in the range [0, 1]. """ hue: float saturation: float value: float class RGB(NamedTuple): """RGB color space named tuple. Used as an attribute in the :obj:`pyvesync.helpers.Color` dataclass. Attributes: red (float): The red component of the RGB color. green (float): The green component of the RGB color. blue (float): The blue component of the RGB color. """ red: float green: float blue: float @dataclass class Color: """Dataclass for color values. For HSV, pass hue as value in degrees 0-360, saturation and value as values between 0 and 100. For RGB, pass red, green and blue as values between 0 and 255. This dataclass provides validation and conversion methods for both HSV and RGB color spaces Notes: To instantiate pass kw arguments for colors with *either* **hue, saturation and value** *or* **red, green and blue**. RGB will take precedence if both are provided. Once instantiated, the named tuples `hsv` and `rgb` will be available as attributes. Args: red (int): Red value of RGB color, 0-255 green (int): Green value of RGB color, 0-255 blue (int): Blue value of RGB color, 0-255 hue (int): Hue value of HSV color, 0-360 saturation (int): Saturation value of HSV color, 0-100 value (int): Value (brightness) value of HSV color, 0-100 Attributes: hsv (namedtuple): hue (0-360), saturation (0-100), value (0-100) see [`HSV dataclass`][pyvesync.helpers.HSV] rgb (namedtuple): red (0-255), green (0-255), blue (0-255) see [`RGB dataclass`][pyvesync.helpers.RGB] """ red: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) green: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) blue: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) hue: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) saturation: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) value: InitVar[NUMERIC] = field(default=None, repr=False, compare=False) hsv: HSV = field(init=False) rgb: RGB = field(init=False) def __post_init__(self, red: NUMERIC, green: NUMERIC, blue: NUMERIC, hue: NUMERIC, saturation: NUMERIC, value: NUMERIC) -> None: """Check HSV or RGB Values and create named tuples.""" if None not in [hue, saturation, value]: self.hsv = HSV(*self.valid_hsv(hue, saturation, value)) # type: ignore[arg-type] # noqa self.rgb = self.hsv_to_rgb(hue, saturation, value) # type: ignore[arg-type] # noqa elif None not in [red, green, blue]: self.rgb = RGB(*self.valid_rgb(red, green, blue)) # type: ignore[arg-type] self.hsv = self.rgb_to_hsv(red, green, blue) # type: ignore[arg-type] else: logger.error('No color values provided') @staticmethod def _min_max(value: float | str, min_val: float, max_val: float, default: float) -> float: """Check if value is within min and max values.""" try: val = max(min_val, (min(max_val, round(float(value), 2)))) except (ValueError, TypeError): val = default return val @classmethod def valid_hsv(cls, h: float | str, s: float | str, v: float | str) -> tuple: """Check if HSV values are valid.""" valid_hue = float(cls._min_max(h, 0, 360, 360)) valid_saturation = float(cls._min_max(s, 0, 100, 100)) valid_value = float(cls._min_max(v, 0, 100, 100)) return ( valid_hue, valid_saturation, valid_value ) @classmethod def valid_rgb(cls, r: float, g: float, b: float) -> list: """Check if RGB values are valid.""" rgb = [] for val in (r, g, b): valid_val = cls._min_max(val, 0, 255, 255) rgb.append(valid_val) return rgb @staticmethod def hsv_to_rgb(hue: float, saturation: float, value: float) -> RGB: """Convert HSV to RGB.""" return RGB( *tuple(round(i * 255, 0) for i in colorsys.hsv_to_rgb( hue / 360, saturation / 100, value / 100 )) ) @staticmethod def rgb_to_hsv(red: float, green: float, blue: float) -> HSV: """Convert RGB to HSV.""" hsv_tuple = colorsys.rgb_to_hsv( red / 255, green / 255, blue / 255 ) hsv_factors = [360, 100, 100] return HSV( float(round(hsv_tuple[0] * hsv_factors[0], 2)), float(round(hsv_tuple[1] * hsv_factors[1], 2)), float(round(hsv_tuple[2] * hsv_factors[2], 0)), ) @dataclass class Timer: """Dataclass to hold state of timers. Note: This should be used by VeSync device instances to manage internal status, does not interact with the VeSync API. Args: timer_duration (int): Length of timer in seconds action (str): Action to perform when timer is done id (int): ID of timer, defaults to 1 remaining (int): Time remaining on timer in seconds, defaults to None update_time (int): Last updated unix timestamp in seconds, defaults to None Attributes: update_time (str): Timestamp of last update status (str): Status of timer, one of 'active', 'paused', 'done' time_remaining (int): Time remaining on timer in seconds running (bool): True if timer is running paused (bool): True if timer is paused done (bool): True if timer is done """ timer_duration: int action: str id: int = 1 remaining: InitVar[int | None] = None _status: str = 'active' _remain: int = 0 update_time: int | None = int(time.time()) def __post_init__(self, remaining: int | None) -> None: """Set remaining time if provided.""" if remaining is not None: self._remain = remaining else: self._remain = self.timer_duration @property def status(self) -> str: """Return status of timer.""" return self._status @status.setter def status(self, status: str) -> None: """Set status of timer.""" if status not in ['active', 'paused', 'done']: logger.error('Invalid status %s', status) raise ValueError self._internal_update() if status == 'done' or self._status == 'done': self.end() return if self.status == 'paused' and status == 'active': self.update_time = int(time.time()) if self.status == 'active' and status == 'paused': self.update_time = None self._status = status @property def _seconds_since_check(self) -> int: """Return seconds since last update.""" if self.update_time is None: return 0 return int(time.time()) - self.update_time @property def time_remaining(self) -> int: """Return remaining seconds.""" self._internal_update() return self._remain @time_remaining.setter def time_remaining(self, remaining: int) -> None: """Set time remaining in seconds.""" if remaining <= 0: self.end() return self._internal_update() if self._status == 'done': self._remain = 0 return self._remain = remaining def _internal_update(self) -> None: """Use time remaining update status.""" if self._status == 'paused': self.update_time = None return if self._status == 'done' or (self._seconds_since_check > self._remain and self._status == 'active'): self._status = 'done' self.update_time = None self._remain = 0 if self._status == 'active': self._remain = self._remain - self._seconds_since_check self.update_time = int(time.time()) @property def running(self) -> bool: """Check if timer is active.""" return (self.time_remaining > 0 and self.status == 'active') @property def paused(self) -> bool: """Check if timer is paused.""" return bool(self.status == 'paused') @property def done(self) -> bool: """Check if timer is complete.""" return bool(self.time_remaining <= 0 or self._status == 'done') def end(self) -> None: """Change status of timer to done.""" self._status = 'done' self._remain = 0 self.update_time = None def start(self) -> None: """Restart paused timer.""" if self._status != 'paused': return self.update_time = int(time.time()) self.status = 'active' def update(self, *, time_remaining: int | None = None, status: str | None = None) -> None: """Update timer. Accepts only KW args Parameters: time_remaining : int Time remaining on timer in seconds status : str Status of timer, can be active, paused, or done """ if time_remaining is not None: self.time_remaining = time_remaining if status is not None: self.status = status def pause(self) -> None: """Pause timer. NOTE - this does not stop the timer via API only locally.""" self._internal_update() if self.status == 'done': return self.status = 'paused' self.update_time = None pyvesync-2.1.18/src/pyvesync/vesync.py000066400000000000000000000402641475441503300200270ustar00rootroot00000000000000"""VeSync API Device Libary.""" import logging import re import time from itertools import chain from typing import Tuple from pyvesync.helpers import Helpers import pyvesync.helpers as helpermodule from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.vesyncbulb import * # noqa: F403, F401 import pyvesync.vesyncbulb as bulb_mods from pyvesync.vesyncfan import * # noqa: F403, F401 import pyvesync.vesyncfan as fan_mods from pyvesync.vesyncoutlet import * # noqa: F403, F401 import pyvesync.vesyncoutlet as outlet_mods from pyvesync.vesyncswitch import * # noqa: F403, F401 import pyvesync.vesynckitchen as kitchen_mods import pyvesync.vesyncswitch as switch_mods logger = logging.getLogger(__name__) API_RATE_LIMIT: int = 30 DEFAULT_TZ: str = 'America/New_York' DEFAULT_ENER_UP_INT: int = 21600 def object_factory(dev_type, config, manager) -> Tuple[str, VeSyncBaseDevice]: """Get device type and instantiate class. Pulls the device types from each module to determine the type of device and instantiates the device object. Args: dev_type (str): Device model type returned from API config (dict): Device configuration from `VeSync.get_devices()` API call manager (VeSync): VeSync manager object Returns: Tuple[str, VeSyncBaseDevice]: Tuple of device type classification and instantiated device object Note: Device types are pulled from the `*_mods` attribute of each device module. See [pyvesync.vesyncbulb.bulb_mods], [pyvesync.vesyncfan.fan_mods], [pyvesync.vesyncoutlet.outlet_mods], [pyvesync.vesyncswitch.switch_mods], and [pyvesync.vesynckitchen.kitchen_mods] for more information. """ def fans(dev_type, config, manager): fan_cls = fan_mods.fan_modules[dev_type] # noqa: F405 fan_obj = getattr(fan_mods, fan_cls) return 'fans', fan_obj(config, manager) def outlets(dev_type, config, manager): outlet_cls = outlet_mods.outlet_modules[dev_type] # noqa: F405 outlet_obj = getattr(outlet_mods, outlet_cls) return 'outlets', outlet_obj(config, manager) def switches(dev_type, config, manager): switch_cls = switch_mods.switch_modules[dev_type] # noqa: F405 switch_obj = getattr(switch_mods, switch_cls) return 'switches', switch_obj(config, manager) def bulbs(dev_type, config, manager): bulb_cls = bulb_mods.bulb_modules[dev_type] # noqa: F405 bulb_obj = getattr(bulb_mods, bulb_cls) return 'bulbs', bulb_obj(config, manager) def kitchen(dev_type, config, manager): kitchen_cls = kitchen_mods.kitchen_modules[dev_type] kitchen_obj = getattr(kitchen_mods, kitchen_cls) return 'kitchen', kitchen_obj(config, manager) if dev_type in fan_mods.fan_modules: # type: ignore # noqa: F405 type_str, dev_obj = fans(dev_type, config, manager) elif dev_type in outlet_mods.outlet_modules: # type: ignore # noqa: F405 type_str, dev_obj = outlets(dev_type, config, manager) elif dev_type in switch_mods.switch_modules: # type: ignore # noqa: F405 type_str, dev_obj = switches(dev_type, config, manager) elif dev_type in bulb_mods.bulb_modules: # type: ignore # noqa: F405 type_str, dev_obj = bulbs(dev_type, config, manager) elif dev_type in kitchen_mods.kitchen_modules: type_str, dev_obj = kitchen(dev_type, config, manager) else: logger.debug('Unknown device named %s model %s', config.get('deviceName', ''), config.get('deviceType', '') ) type_str = 'unknown' dev_obj = None return type_str, dev_obj class VeSync: # pylint: disable=function-redefined """VeSync Manager Class.""" def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False, redact=True): """Initialize VeSync Manager. This class is used as the manager for all VeSync objects, all methods and API calls are performed from this class. Time zone, debug and redact are optional. Time zone must be a string of an IANA time zone format. Once class is instantiated, call `manager.login()` to log in to VeSync servers, which returns `True` if successful. Once logged in, call `manager.update()` to retrieve devices and update device details. Parameters: username : str VeSync account username (usually email address) password : str VeSync account password time_zone : str, optional Time zone for device from IANA database, by default DEFAULT_TZ debug : bool, optional Enable debug logging, by default False redact : bool, optional Redact sensitive information in logs, by default True Attributes: fans : list List of VeSyncFan objects for humidifiers and air purifiers outlets : list List of VeSyncOutlet objects for smart plugs switches : list List of VeSyncSwitch objects for wall switches bulbs : list List of VeSyncBulb objects for smart bulbs kitchen : list List of VeSyncKitchen objects for smart kitchen appliances dev_list : dict Dictionary of device lists token : str VeSync API token account_id : str VeSync account ID enabled : bool True if logged in to VeSync, False if not """ self.debug = debug if debug: # pragma: no cover logger.setLevel(logging.DEBUG) bulb_mods.logger.setLevel(logging.DEBUG) switch_mods.logger.setLevel(logging.DEBUG) outlet_mods.logger.setLevel(logging.DEBUG) fan_mods.logger.setLevel(logging.DEBUG) helpermodule.logger.setLevel(logging.DEBUG) kitchen_mods.logger.setLevel(logging.DEBUG) self._redact = redact if redact: self.redact = redact self.username = username self.password = password self.token = None self.account_id = None self.country_code = None self.devices = None self.enabled = False self.update_interval = API_RATE_LIMIT self.last_update_ts = None self.in_process = False self._energy_update_interval = DEFAULT_ENER_UP_INT self._energy_check = True self._dev_list = {} self.outlets = [] self.switches = [] self.fans = [] self.bulbs = [] self.scales = [] self.kitchen = [] self._dev_list = { 'fans': self.fans, 'outlets': self.outlets, 'switches': self.switches, 'bulbs': self.bulbs, 'kitchen': self.kitchen } if isinstance(time_zone, str) and time_zone: reg_test = r'[^a-zA-Z/_]' if bool(re.search(reg_test, time_zone)): self.time_zone = DEFAULT_TZ logger.debug('Invalid characters in time zone - %s', time_zone) else: self.time_zone = time_zone else: self.time_zone = DEFAULT_TZ logger.debug('Time zone is not a string') @property def debug(self) -> bool: """Return debug flag.""" return self._debug @debug.setter def debug(self, new_flag: bool) -> None: """Set debug flag.""" log_modules = [bulb_mods, switch_mods, outlet_mods, fan_mods, helpermodule] if new_flag: logger.setLevel(logging.DEBUG) for m in log_modules: m.logger.setLevel(logging.DEBUG) elif new_flag is False: logger.setLevel(logging.WARNING) for m in log_modules: m.logger.setLevel(logging.WARNING) self._debug = new_flag @property def redact(self) -> bool: """Return debug flag.""" return self._redact @redact.setter def redact(self, new_flag: bool) -> None: """Set debug flag.""" if new_flag: Helpers.shouldredact = True elif new_flag is False: Helpers.shouldredact = False self._redact = new_flag @property def energy_update_interval(self) -> int: """Return energy update interval.""" return self._energy_update_interval @energy_update_interval.setter def energy_update_interval(self, new_energy_update: int) -> None: """Set energy update interval in seconds.""" if new_energy_update > 0: self._energy_update_interval = new_energy_update @staticmethod def remove_dev_test(device, new_list: list) -> bool: """Test if device should be removed - False = Remove.""" if isinstance(new_list, list) and device.cid: for item in new_list: device_found = False if 'cid' in item: if device.cid == item['cid']: device_found = True break else: logger.debug('No cid found in - %s', str(item)) if not device_found: logger.debug( 'Device removed - %s - %s', device.device_name, device.device_type ) return False return True def add_dev_test(self, new_dev: dict) -> bool: """Test if new device should be added - True = Add.""" if 'cid' in new_dev: for _, v in self._dev_list.items(): for dev in v: if ( dev.cid == new_dev.get('cid') and new_dev.get('subDeviceNo', 0) == dev.sub_device_no ): return False return True def remove_old_devices(self, devices: list) -> bool: """Remove devices not found in device list return.""" for k, v in self._dev_list.items(): before = len(v) v[:] = [x for x in v if self.remove_dev_test(x, devices)] after = len(v) if before != after: logger.debug('%s %s removed', str((before - after)), k) return True @staticmethod def set_dev_id(devices: list) -> list: """Correct devices without cid or uuid.""" dev_num = 0 dev_rem = [] for dev in devices: if dev.get('cid') is None: if dev.get('macID') is not None: dev['cid'] = dev['macID'] elif dev.get('uuid') is not None: dev['cid'] = dev['uuid'] else: dev_rem.append(dev_num) logger.warning('Device with no ID - %s', dev.get('deviceName')) dev_num += 1 if dev_rem: devices = [i for j, i in enumerate( devices) if j not in dev_rem] return devices def process_devices(self, dev_list: list) -> bool: """Instantiate Device Objects. Internal method run by `get_devices()` to instantiate device objects. """ devices = VeSync.set_dev_id(dev_list) num_devices = 0 for _, v in self._dev_list.items(): if isinstance(v, list): num_devices += len(v) else: num_devices += 1 if not devices: logger.warning('No devices found in api return') return False if num_devices == 0: logger.debug('New device list initialized') else: self.remove_old_devices(devices) devices[:] = [x for x in devices if self.add_dev_test(x)] detail_keys = ['deviceType', 'deviceName', 'deviceStatus'] for dev in devices: if not all(k in dev for k in detail_keys): logger.debug('Error adding device') continue dev_type = dev.get('deviceType') try: device_str, device_obj = object_factory(dev_type, dev, self) device_list = getattr(self, device_str) device_list.append(device_obj) except AttributeError as err: logger.debug('Error - %s', err) logger.debug('%s device not added', dev_type) continue return True def get_devices(self) -> bool: """Return tuple listing outlets, switches, and fans of devices. This is an internal method called by `update()` """ if not self.enabled: return False self.in_process = True proc_return = False response, _ = Helpers.call_api( '/cloud/v1/deviceManaged/devices', 'post', headers=Helpers.req_header_bypass(), json_object=Helpers.req_body(self, 'devicelist'), ) if response and Helpers.code_check(response): if 'result' in response and 'list' in response['result']: device_list = response['result']['list'] proc_return = self.process_devices(device_list) else: logger.error('Device list in response not found') else: logger.warning('Error retrieving device list') self.in_process = False return proc_return def login(self) -> bool: """Log into VeSync server. Username and password are provided when class is instantiated. Returns: True if login successful, False if not """ user_check = isinstance(self.username, str) and len(self.username) > 0 pass_check = isinstance(self.password, str) and len(self.password) > 0 if user_check is False: logger.error('Username invalid') return False if pass_check is False: logger.error('Password invalid') return False response, _ = Helpers.call_api( '/cloud/v1/user/login', 'post', json_object=Helpers.req_body(self, 'login') ) if Helpers.code_check(response) and 'result' in response: self.token = response.get('result').get('token') self.account_id = response.get('result').get('accountID') self.country_code = response.get('result').get('countryCode') self.enabled = True logger.debug('Login successful') logger.debug('token %s', self.token) logger.debug('account_id %s', self.account_id) return True logger.error('Error logging in with username and password') return False def device_time_check(self) -> bool: """Test if update interval has been exceeded.""" return ( self.last_update_ts is None or (time.time() - self.last_update_ts) > self.update_interval ) def update(self) -> None: """Fetch updated information about devices. Pulls devices list from VeSync and instantiates any new devices. Devices are stored in the instance attributes `outlets`, `switches`, `fans`, and `bulbs`. The `_device_list` attribute is a dictionary of these attributes. """ if self.device_time_check(): if not self.enabled: logger.error('Not logged in to VeSync') return self.get_devices() devices = list(self._dev_list.values()) logger.debug('Start updating the device details one by one') for device in chain(*devices): device.update() self.last_update_ts = time.time() def update_energy(self, bypass_check=False) -> None: """Fetch updated energy information for outlet devices.""" if self.outlets: for outlet in self.outlets: outlet.update_energy(bypass_check) def update_all_devices(self) -> None: """Run `get_details()` for each device and update state.""" devices = list(self._dev_list.values()) for dev in chain(*devices): dev.update() pyvesync-2.1.18/src/pyvesync/vesyncbasedevice.py000066400000000000000000000172711475441503300220440ustar00rootroot00000000000000"""Base class for all VeSync devices.""" from __future__ import annotations import logging import json from typing import TYPE_CHECKING from pyvesync.helpers import Helpers as helper # noqa: N813 logger = logging.getLogger(__name__) if TYPE_CHECKING: from pyvesync import VeSync class VeSyncBaseDevice: """Properties shared across all VeSync devices. Base class for all VeSync devices. Parameters: details (dict): Device details from API call. manager (VeSync): Manager object for API calls. Attributes: device_name (str): Name of device. device_image (str): URL for device image. cid (str): Device ID. connection_status (str): Connection status of device. 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. mode (str): Mode of device. speed (Union[str, int]): Speed of device. extension (dict): Extension of device, not used. 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. config (dict): Configuration of device, including firmware version device_status (str): Status of device, on or off. Methods: is_on(): Return True if device is on. firmware_update(): Return True if firmware update available. display(): Print formatted device info to stdout. displayJSON(): JSON API for device details. """ def __init__(self, details: dict, manager: VeSync) -> None: """Initialize VeSync device base class.""" self.manager = manager if 'cid' in details and details['cid'] is not None: self.device_name: str = details['deviceName'] self.device_image: str | None = details.get('deviceImg') self.cid: str = details['cid'] self.connection_status: str = details['connectionStatus'] self.connection_type: str | None = details.get( 'connectionType') self.device_type: str = details['deviceType'] self.type: str | None = details.get('type') self.uuid: str | None = details.get('uuid') self.config_module: str = details['configModule'] self.mac_id: str | None = details.get('macID') self.mode: str | None = details.get('mode') self.speed: int | None = details.get('speed') if details.get( 'speed') != '' else None self.extension = details.get('extension') self.current_firm_version = details.get( 'currentFirmVersion') self.device_region: str | None = details.get('deviceRegion') self.pid = None self.sub_device_no = details.get('subDeviceNo', 0) self.config: dict = {} if isinstance(details.get('extension'), dict): ext = details['extension'] self.speed = ext.get('fanSpeedLevel') self.mode = ext.get('mode') if self.connection_status != 'online': self.device_status: str | None = 'off' else: self.device_status = details.get('deviceStatus') else: logger.error('No cid found for %s', self.__class__.__name__) def __eq__(self, other: object) -> bool: """Use device CID and subdevice number to test equality.""" if not isinstance(other, VeSyncBaseDevice): return NotImplemented return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no) def __hash__(self) -> int: """Use CID and sub-device number to make device hash.""" if isinstance(self.sub_device_no, int) and self.sub_device_no > 0: return hash(self.cid + str(self.sub_device_no)) return hash(self.cid) def __str__(self) -> str: """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}' def __repr__(self) -> str: """Representation of device details.""" return f'DevClass: {self.__class__.__name__},\ Name:{self.device_name}, Device No: {self.sub_device_no},\ DevStatus: {self.device_status}, CID: {self.cid}' @property def is_on(self) -> bool: """Return true if device is on.""" return (self.device_status == 'on') @property def firmware_update(self) -> bool: """Return True if firmware update available.""" cfv = self.config.get('current_firmware_version') lfv = self.config.get('latest_firmware_version') if cfv is not None and lfv is not None: if cfv != lfv: return True else: logger.debug('Call device.get_config() to get firmware versions') return False def get_pid(self) -> None: """Get managed device configuration.""" body = helper.req_body(self.manager, 'devicedetail') body['configModule'] = self.config_module body['region'] = self.device_region body['method'] = 'configInfo' r, _ = helper.call_api('/cloud/v1/deviceManaged/configInfo', 'post', json_object=body) if not isinstance(r, dict) or r.get('code') != 0 or r.get('result') is None: logger.error('Error getting config info for %s', self.device_name) return self.pid = r.get('result', {}).get('pid') def display(self) -> None: """Print formatted device info to stdout. Example: ``` Device Name:..................Living Room Lamp Model:........................ESL100 Subdevice No:.................0 Status:.......................on Online:.......................online Type:.........................wifi CID:..........................1234567890abcdef ``` """ disp = [ ('Device Name:', self.device_name), ('Model: ', self.device_type), ('Subdevice No: ', str(self.sub_device_no)), ('Status: ', self.device_status), ('Online: ', self.connection_status), ('Type: ', self.type), ('CID: ', self.cid), ] if self.uuid is not None: disp.append(('UUID: ', self.uuid)) for line in disp: print(f'{line[0]:.<30} {line[1]}') def displayJSON(self) -> str: # pylint: disable=invalid-name """JSON API for device details. Returns: str: JSON formatted string of device details. Example: ``` { "Device Name": "Living Room Lamp", "Model": "ESL100", "Subdevice No": "0", "Status": "on", "Online": "online", "Type": "wifi", "CID": "1234567890abcdef" } ``` """ return json.dumps( { 'Device Name': self.device_name, 'Model': self.device_type, 'Subdevice No': str(self.sub_device_no), 'Status': self.device_status, 'Online': self.connection_status, 'Type': self.type, 'CID': self.cid, }, indent=4) pyvesync-2.1.18/src/pyvesync/vesyncbulb.py000066400000000000000000001455201475441503300206750ustar00rootroot00000000000000"""Etekcity/Valceno Smart Light Bulbs. This module provides classes for the following Etekcity/Valceno smart lights: 1. ESL100: Dimmable Bulb 2. ESL100CW: Tunable White Bulb 3. XYD0001: RGB Bulb 4. ESL100MC: Multi-Color Bulb Attributes: feature_dict (dict): Dictionary of bulb models and their supported features. Defines the class to use for each bulb model and the list of features bulb_modules (dict): Dictionary of bulb models as keys and their associated classes as string values. Note: The bulb module is built from the `feature_dict` dictionary and used by the `vesync.object_factory` and tests to determine the class to instantiate for each bulb model. These classes should not be instantiated manually. Examples: The following example shows the structure of the `feature_dict` dictionary: ```python feature_dict = { 'ESL100MC': { # device_type attribute 'module': 'VeSyncBulbESL100MC', # String name of the class to instantiate 'features': ['dimmable', 'rgb_shift'], # List of supported features 'color_model': 'rgb' # Color model used by the bulb (rgb, hsv, none) } } ``` """ from __future__ import annotations import logging import json from typing import Union, Optional, NamedTuple, TYPE_CHECKING from abc import ABCMeta, abstractmethod from pyvesync.helpers import Helpers as helpers, Color from pyvesync.vesyncbasedevice import VeSyncBaseDevice if TYPE_CHECKING: from pyvesync import VeSync logger = logging.getLogger(__name__) NUMERIC_T = Optional[Union[int, float, str]] # --8<-- [start:feature_dict] feature_dict: dict = { 'ESL100': { 'module': 'VeSyncBulbESL100', 'features': ['dimmable'], 'color_model': 'none' }, 'ESL100CW': { 'module': 'VeSyncBulbESL100CW', 'features': ['dimmable', 'color_temp'], 'color_model': 'none' }, 'XYD0001': { 'module': 'VeSyncBulbValcenoA19MC', 'features': ['dimmable', 'color_temp', 'rgb_shift'], 'color_model': 'hsv' }, 'ESL100MC': { 'module': 'VeSyncBulbESL100MC', 'features': ['dimmable', 'rgb_shift'], 'color_model': 'rgb' } } # --8<-- [end:feature_dict] bulb_modules: dict = {k: v['module'] for k, v in feature_dict.items()} __all__: list = list(bulb_modules.values()) + ["bulb_modules", "VeSyncBulb"] def pct_to_kelvin(pct: float, max_k: int = 6500, min_k: int = 2700) -> float: """Convert percent to kelvin.""" return ((max_k - min_k) * pct / 100) + min_k class VeSyncBulb(VeSyncBaseDevice): """Base class for VeSync Bulbs. Abstract base class to provide methods for controlling and getting details of VeSync bulbs. Inherits from [`VeSyncBaseDevice`][pyvesync.vesyncbasedevice.VeSyncBaseDevice]. This class should not be used directly for devices, but rather subclassed for each. Attributes: brightness (int): Brightness of bulb (0-100). color_temp_kelvin (int): White color temperature of bulb in Kelvin. color_temp_pct (int): White color temperature of bulb in percent (0-100). color_hue (float): Color hue of bulb (0-360). color_saturation (float): Color saturation of bulb in percent (0-100). color_value (float): Color value of bulb in percent (0-100). color (Color): Color of bulb in the form of a dataclass with two namedtuple attributes - `hsv` & `rgb`. See [pyvesync.helpers.Color][]. """ __metaclass__ = ABCMeta def __init__(self, details: dict[str, str | list], manager: VeSync) -> None: """Initialize VeSync smart bulb base class.""" super().__init__(details, manager) self._brightness = 0 self._color_temp = 0 self._color_value = float(0) self._color_hue = float(0) self._color_saturation = float(0) self._color_mode: str = '' # possible: white, color, hsv self._color: Color | None = None self.features: list | None = feature_dict.get( self.device_type, {}).get('features') if self.features is None: logger.error("No configuration set for - %s", self.device_name) raise KeyError self._rgb_values = { 'red': 0, 'green': 0, 'blue': 0 } @property def brightness(self) -> int: """Return brightness of vesync bulb. Returns: int: Brightness of bulb (0-100). """ if self.dimmable_feature and self._brightness is not None: return self._brightness return 0 @property def color_temp_kelvin(self) -> int: """Return white color temperature of bulb in Kelvin. Converts the color temperature in percent to Kelvin using the `pct_to_kelvin` function. Returns: int: White color temperature of bulb in Kelvin (2700 - 6500). Notes: This returns 0 for bulbs that do not have color temperature support. """ if self.color_temp_feature and self._color_temp is not None: return int(pct_to_kelvin(self._color_temp)) return 0 @property def color_temp_pct(self) -> int: """Return white color temperature of bulb in percent (0-100). Subclasses that use this method, should calculate the color temeprature in percent regardless of how the API returns the value. """ if self.color_temp_feature and self._color_temp is not None: return int(self._color_temp) return 0 @property def color_hue(self) -> float: """Return color hue (HSV colorspace) of bulb. Returns hue from the `color` attribute. (0-360) Returns: float: Color hue of bulb in HSV colorspace. Notes: This returns 0 for bulbs that do not have color support. """ if self.rgb_shift_feature and self._color is not None: return self._color.hsv.hue return 0 @property def color_saturation(self) -> float: """Return color saturation (HSV colorspace) of bulb in percent. Return saturation from the `color` attribute (0-100). Returns: float: Color saturation of bulb in percent (0-100). Notes: This returns 0 for bulbs that do not have color """ if self.rgb_shift_feature and self._color is not None: return self._color.hsv.saturation return 0 @property def color_value(self) -> float: """Return color value (HSV colorspace) of bulb in percent. Returns the color from from the `color` attribute (0-100). Returns: float: Color value of bulb in percent (0-100). Notes: This returns 0 for bulbs that do not have color support. """ if self.rgb_shift_feature and self._color is not None: return self._color.hsv.value return 0 @property # pylint: disable-next=differing-param-doc # DOCUMENTATION FOR SETTER def color(self) -> Color | None: # pylint: disable=differing-type-doc """Set color property based on rgb or hsv values. Pass either red, green, blue or hue, saturation, value. Args: red (float): Red value of RGB color, 0-255 green (float): Green value of RGB color, 0-255 blue (float): Blue value of RGB color, 0-255 hue (float): Hue value of HSV color, 0-360 saturation (float): Saturation value of HSV color, 0-100 value (float): Value (brightness) value of HSV color 0-100 Returns: Color: Color dataclass with hsv and rgb named tuple attributes. """ if self.rgb_shift_feature is True and self._color is not None: return self._color return None @color.setter def color(self, red: float | None = None, green: float | None = None, blue: float | None = None, hue: float | None = None, saturation: float | None = None, value: float | None = None) -> None: """Set color property based on rgb or hsv values.""" self._color = Color(red=red, green=green, blue=blue, hue=hue, saturation=saturation, value=value) @property def color_hsv(self) -> NamedTuple | None: """Return color of bulb as [hsv named tuple][pyvesync.helpers.HSV]. Notes: Returns `None` for bulbs that do not have color support. """ if self.rgb_shift_feature is True and self._color is not None: return self._color.hsv return None @property def color_rgb(self) -> NamedTuple | None: """Return color of bulb as [rgb named tuple][pyvesync.helpers.RGB]. Notes: Returns `None` for bulbs that do not have color support. """ if self.rgb_shift_feature is True and self._color is not None: return self._color.rgb return None @property def color_mode(self) -> str | None: """Return color mode of bulb. Possible values are none, hsv or rgb. Notes: This is a read-only property. Color mode is defined in the [`feature_dict`][pyvesync.vesyncbulb.feature_dict]. """ if self.rgb_shift_feature and self._color_mode is not None: return str(self._color_mode) return None @property def dimmable_feature(self) -> bool: """Return true if dimmable bulb.""" return (self.features is not None and 'dimmable' in self.features) @property def color_temp_feature(self) -> bool: """Checks if the device has the ability to change color temperature. Returns: bool: True if the device supports changing color temperature. """ return (self.features is not None and 'color_temp' in self.features) @property def rgb_shift_feature(self) -> bool: """Checks if the device is multicolor. Returns: bool: True if the device supports changing color. """ return (self.features is not None and 'rgb_shift' in self.features) def _validate_rgb(self, red: NUMERIC_T = None, green: NUMERIC_T = None, blue: NUMERIC_T = None) -> Color: """Validate RGB values.""" rgb_dict = {'red': red, 'green': green, 'blue': blue} for clr, val in rgb_dict.items(): if val is None: rgb_dict[clr] = getattr(self._rgb_values, clr) else: rgb_dict[clr] = val return Color(red=rgb_dict['red'], green=rgb_dict['green'], blue=rgb_dict['blue']) def _validate_hsv(self, hue: NUMERIC_T = None, saturation: NUMERIC_T = None, value: NUMERIC_T = None) -> Color: """Validate HSV Arguments.""" hsv_dict = {'hue': hue, 'saturation': saturation, 'value': value} for clr, val in hsv_dict.items(): if val is None and self._color is not None: hsv_dict[clr] = getattr(self._color.hsv, clr) if hue is not None: valid_hue = self._validate_any(hue, 1, 360, 360) elif self._color is not None: valid_hue = self._color.hsv.hue else: logger.debug("No current hue value, setting to 0") valid_hue = 360 hsv_dict['hue'] = valid_hue for itm, val in {'saturation': saturation, 'value': value}.items(): if val is not None: valid_item = self._validate_any(val, 1, 100, 100) elif self.color is not None: valid_item = getattr(self.color.hsv, itm) else: logger.debug("No current %s value, setting to 0", itm) valid_item = 100 hsv_dict[itm] = valid_item return Color(hue=hsv_dict['hue'], saturation=hsv_dict['saturation'], value=hsv_dict['value']) def _validate_brightness(self, brightness: float | str, start: int = 0, stop: int = 100) -> int: """Validate brightness value.""" try: brightness_update: int = max(start, (min(stop, int( round(float(brightness), 2))))) except (ValueError, TypeError): brightness_update = self.brightness if self.brightness is not None else 100 return brightness_update def _validate_color_temp(self, temp: int, start: int = 0, stop: int = 100) -> int: """Validate color temperature.""" try: temp_update = max(start, (min(stop, int( round(float(temp), 0))))) except (ValueError, TypeError): temp_update = self._color_temp if self._color_temp is not None else 100 return temp_update @staticmethod def _validate_any(value: NUMERIC_T, start: NUMERIC_T = 0, stop: NUMERIC_T = 100, default: float = 100) -> float: """Validate any value.""" try: value_update = max(float(start), (min(float(stop), round(float(value), 2)))) # type: ignore[arg-type] # noqa except (ValueError, TypeError): value_update = default return value_update @abstractmethod def set_status(self) -> bool: """Set vesync bulb attributes(brightness, color_temp, etc). This is a helper function that is called by the direct `set_*` methods, such as `set_brightness`, `set_rgb`, `set_hsv`, etc. Returns: bool : True if successful, False otherwise. """ @abstractmethod def get_details(self) -> None: """Get vesync bulb details. This is a legacy function to update devices, **updates should be called by `update()`** Returns: None """ @abstractmethod def _interpret_apicall_result(self, response: dict) -> None: """Update bulb status from any api call response.""" @abstractmethod def toggle(self, status: str) -> bool: """Toggle mode of vesync lightbulb. Helper function called by `turn_on()` and `turn_off()`. Args: status (str): 'on' or 'off' Returns: bool: True if successful, False otherwise. """ @abstractmethod def get_config(self) -> None: """Call api to get configuration details and firmware. Populates the `self.config` attribute with the response. Returns: None Note: The configuration attribute `self.config` is structured as follows: ```python { 'current_firmware_version': '1.0.0', 'latest_firmware_version': '1.0.0', 'maxPower': '560', 'threshold': '1000', 'power_protection': 'on', 'energy_saving_status': 'on' } ``` """ def set_hsv(self, hue: NUMERIC_T, saturation: NUMERIC_T, value: NUMERIC_T) -> bool | None: """Set HSV if supported by bulb. Args: hue (NUMERIC_T): Hue 0-360 saturation (NUMERIC_T): Saturation 0-100 value (NUMERIC_T): Value 0-100 Returns: bool: True if successful, False otherwise. """ if self.rgb_shift_feature is False: logger.debug("HSV not supported by bulb") return False return bool(hue and saturation and value) def set_rgb(self, red: NUMERIC_T = None, green: NUMERIC_T = None, blue: NUMERIC_T = None) -> bool: """Set RGB if supported by bulb. Args: red (NUMERIC_T): Red 0-255 green (NUMERIC_T): green 0-255 blue (NUMERIC_T): blue 0-255 Returns: bool: True if successful, False otherwise. """ if self.rgb_shift_feature is False: logger.debug("RGB not supported by bulb") return False return bool(red and green and blue) def turn_on(self) -> bool: """Turn on vesync bulbs. Calls `toggle('on')`. Returns: bool : True if successful, False otherwise. """ if self.toggle('on'): self.device_status = 'on' return True logger.warning('Error turning %s on', self.device_name) return False def turn_off(self) -> bool: """Turn off vesync bulbs. Calls `toggle('off')`. Returns: bool : True if successful, False otherwise. """ if self.toggle('off'): self.device_status = 'off' return True logger.warning('Error turning %s off', self.device_name) return False def update(self) -> None: """Update bulb details. Calls `get_details()` method to retrieve status from API and update the bulb attributes. `get_details()` is overriden by subclasses to hit the respective API endpoints. """ self.get_details() def display(self) -> None: """Return formatted bulb info to stdout.""" super().display() if self.connection_status == 'online': disp = [] # initiate list if self.dimmable_feature: disp.append(('Brightness: ', str(self.brightness), '%')) if self.color_temp_feature: disp.append(('White Temperature Pct: ', str(self.color_temp_pct), '%')) disp.append(('White Temperature Kelvin: ', str(self.color_temp_kelvin), 'K')) if self.rgb_shift_feature and self.color is not None: disp.append(('ColorHSV: ', helpers.named_tuple_to_str( self.color.hsv), '')) disp.append(('ColorRGB: ', helpers.named_tuple_to_str( self.color.rgb), '')) disp.append(('ColorMode: ', str(self.color_mode), '')) if len(disp) > 0: for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') def displayJSON(self) -> str: """Return bulb device info in JSON format. Returns: str: JSON formatted string of bulb details. Example: ```json { "deviceName": "Bulb", "deviceStatus": "on", "connectionStatus": "online", "Brightness": "100%", "WhiteTemperaturePct": "100%", "WhiteTemperatureKelvin": "6500K", "ColorHSV": "{"hue": 0, "saturation": 0, "value": 0}", "ColorRGB": "{"red": 0, "green": 0, "blue": 0}", "ColorMode": "hsv" } ``` """ sup = super().displayJSON() sup_val = json.loads(sup) if self.connection_status == 'online': if self.dimmable_feature: sup_val.update({'Brightness': str(self.brightness)}) if self.color_temp_feature: sup_val.update( {'WhiteTemperaturePct': str(self.color_temp_pct)}) sup_val.update( {'WhiteTemperatureKelvin': str(self.color_temp_kelvin)}) if self.rgb_shift_feature: if self.color_hsv is not None: sup_val.update({'ColorHSV': json.dumps( self.color_hsv._asdict())}) if self.color_rgb is not None: sup_val.update({'ColorRGB': json.dumps( self.color_rgb._asdict())}) sup_val.update({'ColorMode': str(self.color_mode)}) return json.dumps(sup_val, indent=4) @property def color_value_rgb(self) -> NamedTuple | None: """Legacy Method .... Depreciated.""" if self._color is not None: return self._color.rgb return None @property def color_value_hsv(self) -> NamedTuple | None: """Legacy Method .... Depreciated.""" if self._color is not None: return self._color.hsv return None class VeSyncBulbESL100MC(VeSyncBulb): """Etekcity ESL100 Multi Color Bulb device instance. Inherits from [VeSyncBulb][pyvesync.vesyncbulb.VeSyncBulb] and [VeSyncBaseDevice][pyvesync.vesyncbasedevice.VeSyncBaseDevice]. Attributes: device_status (str): Status of bulb, either 'on' or 'off'. connection_status (str): Connection status of bulb, either 'online' or 'offline'. details (dict): Dictionary of bulb state details. brightness (int): Brightness of bulb (0-100). color_temp_kelvin (int): White color temperature of bulb in Kelvin. color_temp_pct (int): White color temperature of bulb in percent (0-100). color_hue (float): Color hue of bulb (0-360). color_saturation (float): Color saturation of bulb in percent (0-100). color_value (float): Color value of bulb in percent (0-100). color (Color): Color of bulb in the form of a dataclass with two named tuple attributes - `hsv` & `rgb`. See [pyvesync.helpers.Color][]. Notes: The details dictionary contains the device information retreived by the `update()` method: ```python details = { 'brightness': 50, 'colorMode': 'rgb', 'color' : Color(red=0, green=0, blue=0) } ``` See pyvesync.helpers.Color for more information on the Color dataclass. """ def __init__(self, details: dict[str, str | list], manager: VeSync) -> None: """Instantiate ESL100MC Multicolor Bulb. Args: details (dict): Dictionary of bulb state details. manager (VeSync): Manager class used to make API calls. """ super().__init__(details, manager) self.details: dict = {} def get_details(self) -> None: head = helpers.bypass_header() body = helpers.bypass_body_v2(self.manager) body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'getLightStatus', 'source': 'APP', 'data': {} } r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if not isinstance(r, dict) or not isinstance(r.get('result'), dict) \ or r.get('code') != 0: logger.debug("Error in bulb response") return outer_result = r.get('result', {}) inner_result = outer_result.get('result') if inner_result is None or outer_result.get('code') != 0: logger.debug("No status data in bulb response") return self._interpret_apicall_result(inner_result) return def _interpret_apicall_result(self, response: dict) -> None: """Build detail dictionary from response.""" self._brightness = response.get('brightness', 0) self._color_mode = response.get('colorMode', '') self._color = Color(red=response.get('red', 0), green=response.get('green', 0), blue=response.get('blue', 0)) def set_brightness(self, brightness: int) -> bool: """Set brightness of bulb. Calls the `set_status` method with the brightness value. Args: brightness (int): Brightness of bulb (0-100). Returns: bool: True if successful, False otherwise. """ return self.set_status(brightness=brightness) def set_rgb_color(self, red: NUMERIC_T, green: NUMERIC_T, blue: NUMERIC_T) -> bool: """DEPRECIATED, USE `set_rgb()`.""" return self.set_status(red=red, green=green, blue=blue) def set_rgb(self, red: NUMERIC_T = None, green: NUMERIC_T = None, blue: NUMERIC_T = None) -> bool: return self.set_status(red=red, green=green, blue=blue) def set_hsv(self, hue: NUMERIC_T, saturation: NUMERIC_T, value: NUMERIC_T) -> bool | None: rgb = Color(hue=hue, saturation=saturation, value=value).rgb return self.set_status(red=rgb.red, green=rgb.green, blue=rgb.blue) def enable_white_mode(self) -> bool: """Enable white mode on bulb. Returns: bool: True if successful, False otherwise. """ return self.set_status(brightness=100) def set_status(self, brightness: NUMERIC_T = None, red: NUMERIC_T = None, green: NUMERIC_T = None, blue: NUMERIC_T = None) -> bool: """Set color of VeSync ESL100MC. Brightness or RGB values must be provided. If RGB values are provided, brightness is ignored. Args: brightness (int): Brightness of bulb (0-100). red (int): Red value of RGB color, 0-255. green (int): Green value of RGB color, 0-255. blue (int): Blue value of RGB color, 0-255. Returns: bool: True if successful, False otherwise. """ brightness_update = 100 if red is not None and green is not None and blue is not None: new_color = self._validate_rgb(red, green, blue) color_mode = 'color' if self.device_status == 'on' and new_color == self._color: logger.debug("New color is same as current color") return True else: logger.debug("RGB Values not provided") new_color = None if brightness is not None: brightness_update = int(self._validate_brightness(brightness)) # Do nothing if brightness is passed and same as current if self.device_status == 'on' and brightness_update == self._brightness: logger.debug('Brightness already set to %s', brightness) return True color_mode = 'white' else: logger.debug("Brightness and RGB values are not set") return False head = helpers.bypass_header() body = helpers.bypass_body_v2(self.manager) body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'setLightStatus', 'source': 'APP', 'data': { 'action': 'on', 'speed': 0, 'brightness': brightness_update, 'red': 0 if new_color is None else int(new_color.rgb.red), 'green': 0 if new_color is None else int(new_color.rgb.green), 'blue': 0 if new_color is None else int(new_color.rgb.blue), 'colorMode': 'color' if new_color is not None else 'white', } } r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if not isinstance(r, dict) or r.get('code') != 0: logger.debug("Error in setting bulb status") return False if color_mode == 'color' and new_color is not None: self._color_mode = 'color' self._color = Color(red=new_color.rgb.red, green=new_color.rgb.green, blue=new_color.rgb.blue) elif brightness is not None: self._brightness = int(brightness_update) self._color_mode = 'white' self.device_status = 'on' return True def toggle(self, status: str) -> bool: if status == 'on': turn_on = True elif status == 'off': turn_on = False else: logger.debug("Status must be on or off") return False head = helpers.bypass_header() body = helpers.bypass_body_v2(self.manager) body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'source': 'APP', 'method': 'setSwitch', 'data': { 'id': 0, 'enabled': turn_on } } r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if not isinstance(r, dict) or r.get('code') != 0: logger.debug("Error in setting bulb status") return False if turn_on is True: self.device_status = 'on' else: self.device_status = 'off' return True class VeSyncBulbESL100(VeSyncBulb): """Object to hold VeSync ESL100 light bulb. This bulb only has the dimmable feature. Inherits from pyvesync.vesyncbulb.VeSyncBulb and pyvesync.vesyncbasedevice.VeSyncBaseDevice. Attributes: details (dict): Dictionary of bulb state details. brightness (int): Brightness of bulb (0-100). device_status (str): Status of bulb (on/off). connection_status (str): Connection status of bulb (online/offline). """ def __init__(self, details: dict, manager: VeSync) -> None: """Initialize Etekcity ESL100 Dimmable Bulb. Args: details (dict): Dictionary of bulb state details. manager (VeSync): Manager class used to make API calls """ super().__init__(details, manager) self.details: dict = {} def get_details(self) -> None: body = helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid r, _ = helpers.call_api( '/SmartBulb/v1/device/devicedetail', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.connection_status = r.get('connectionStatus') self.device_status = r.get('deviceStatus') if self.dimmable_feature: self._brightness = int(r.get('brightNess')) else: logger.debug('Error getting %s details', self.device_name) def get_config(self) -> None: body = helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = helpers.call_api( '/SmartBulb/v1/device/configurations', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.config = helpers.build_config_dict(r) else: logger.debug('Error getting %s config info', self.device_name) def toggle(self, status: str) -> bool: body = helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = status r, _ = helpers.call_api( '/SmartBulb/v1/device/devicestatus', 'put', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.device_status = status return True return False def set_brightness(self, brightness: int) -> bool: """Set brightness of dimmable bulb. Args: brightness (int): Brightness of bulb (0-100). Returns: bool: True if successful, False otherwise. """ if not self.dimmable_feature: logger.debug('%s is not dimmable', self.device_name) return False brightness_update = int(self._validate_brightness(brightness)) if self.device_status == 'on' and brightness_update == self._brightness: logger.debug("Device already in requested state") return True if self.device_status == 'off': self.toggle('on') body = helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'on' body['brightNess'] = str(brightness_update) r, _ = helpers.call_api( '/SmartBulb/v1/device/updateBrightness', 'put', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self._brightness = brightness_update return True logger.debug('Error setting brightness for %s', self.device_name) return False class VeSyncBulbESL100CW(VeSyncBulb): """VeSync Tunable and Dimmable White Bulb.""" def __init__(self, details: dict, manager: VeSync) -> None: """Initialize Etekcity Tunable white bulb.""" super().__init__(details, manager) def get_details(self) -> None: body = helpers.req_body(self.manager, 'bypass') body['cid'] = self.cid body['jsonCmd'] = {'getLightStatus': 'get'} body['configModule'] = self.config_module r, _ = helpers.call_api( '/cloud/v1/deviceManaged/bypass', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if not isinstance(r, dict) or not helpers.code_check(r): logger.debug('Error calling %s', self.device_name) return light_resp = r.get('result', {}).get('light') if light_resp is not None: self._interpret_apicall_result(light_resp) elif r.get('code') == -11300027: logger.debug('%s device offline', self.device_name) self.connection_status = 'offline' self.device_status = 'off' else: logger.debug( '%s - Unknown return code - %s with message %s', self.device_name, str(r.get('code', '')), str(r.get('msg', '')), ) def _interpret_apicall_result(self, response: dict) -> None: self.connection_status = 'online' self.device_status = response.get('action', 'off') self._brightness = response.get('brightness', 0) self._color_temp = response.get('colorTempe', 0) def get_config(self) -> None: body = helpers.req_body(self.manager, 'bypass_config') body['uuid'] = self.uuid r, _ = helpers.call_api( '/cloud/v1/deviceManaged/configurations', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.config = helpers.build_config_dict(r) else: logger.debug('Error getting %s config info', self.device_name) def toggle(self, status: str) -> bool: if status not in ('on', 'off'): logger.debug('Invalid status %s', status) return False body = helpers.req_body(self.manager, 'bypass') body['cid'] = self.cid body['configModule'] = self.config_module body['jsonCmd'] = {'light': {'action': status}} r, _ = helpers.call_api( '/cloud/v1/deviceManaged/bypass', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.device_status = status return True logger.debug('%s offline', self.device_name) self.device_status = 'off' self.connection_status = 'offline' return False def set_brightness(self, brightness: int) -> bool: """Set brightness of tunable bulb.""" brightness_update = int(self._validate_brightness(brightness)) if self.device_status == 'on' and brightness_update == self._brightness: logger.debug("Device already in requested state") return True body = helpers.req_body(self.manager, 'bypass') body['cid'] = self.cid body['configModule'] = self.config_module light_dict: dict[str, NUMERIC_T] = { 'brightness': brightness_update} if self.device_status == 'off': light_dict['action'] = 'on' body['jsonCmd'] = {'light': light_dict} r, _ = helpers.call_api( '/cloud/v1/deviceManaged/bypass', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self._brightness = brightness_update self.device_status = 'on' self.connection_status = 'online' return True self.device_status = 'off' self.connection_status = 'offline' logger.debug('%s offline', self.device_name) return False def set_color_temp(self, color_temp: int) -> bool: """Set Color Temperature of Bulb in pct (1 - 100).""" color_temp_update = self._validate_color_temp(color_temp) if self.device_status == 'on' and color_temp_update == self._color_temp: logger.debug("Device already in requested state") return True body = helpers.req_body(self.manager, 'bypass') body['cid'] = self.cid body['jsonCmd'] = {'light': {}} body['jsonCmd']['light']['colorTempe'] = color_temp_update if self.device_status == 'off': body['jsonCmd']['light']['action'] = 'on' r, _ = helpers.call_api( '/cloud/v1/deviceManaged/bypass', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if not helpers.code_check(r): return False if r.get('code') == -11300027: logger.debug('%s device offline', self.device_name) self.connection_status = 'offline' self.device_status = 'off' return False if r.get('code') == 0: self.device_status = 'on' self._color_temp = color_temp return True logger.debug( '%s - Unknown return code - %d with message %s', self.device_name, r.get('code'), r.get('msg'), ) return False class VeSyncBulbValcenoA19MC(VeSyncBulb): """VeSync Multicolor Bulb.""" def __init__(self, details: dict, manager: VeSync) -> None: """Initialize Multicolor bulb.""" super().__init__(details, manager) def get_details(self) -> None: body = helpers.req_body(self.manager, 'bypassV2') body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'getLightStatusV2', 'source': 'APP', 'data': {}, } r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=helpers.req_header_bypass(), json_object=body, ) if not isinstance(r, dict) or not helpers.code_check(r): logger.debug('Error calling %s', self.device_name) return self._interpret_apicall_result(r) def _interpret_apicall_result(self, response: dict) -> None: if response.get('result', {}).get('result') is not None: innerresult = response.get('result', {}).get('result') self.connection_status = 'online' self.device_status = innerresult.get('enabled', 'off') if self.dimmable_feature: self._brightness = innerresult.get('brightness') if self.color_temp_feature: self._color_temp = innerresult.get('colorTemp') if self.rgb_shift_feature: self._color_mode = innerresult.get('colorMode') hue = float(round(innerresult.get('hue')/27.777777, 2)) sat = float(innerresult.get('saturation')/100) val = float(innerresult.get('value')) self._color = Color(hue=hue, saturation=sat, value=val) elif (response.get('code') == -11300030 or response.get('code') == -11302030): logger.debug('%s device request timeout', self.device_name) self.connection_status = 'offline' self.device_status = 'off' elif response.get('code') == -11300027: logger.debug('%s device offline', self.device_name) self.connection_status = 'offline' self.device_status = 'off' else: logger.debug( '%s - Unknown return code - %d with message %s', self.device_name, response.get('code'), response.get('msg'), ) def get_config(self) -> None: body = helpers.req_body(self.manager, 'bypass') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = helpers.call_api( '/cloud/v1/deviceManaged/configurations', 'post', headers=helpers.req_header_bypass(), json_object=body, ) if helpers.code_check(r): if r.get('result') is not None: result = r.get('result') self.__build_config_dict(result) else: logger.debug('Error getting %s config info', self.device_name) logger.debug(' return code - %d with message %s', r.get('code'), r.get('msg')) def __build_config_dict(self, conf_dict: dict[str, str]) -> None: """Build configuration dict for Multicolor bulb.""" self.config['currentFirmVersion'] = ( conf_dict.get('currentFirmVersion', '')) self.config['latestFirmVersion'] = ( conf_dict.get('latestFirmVersion', '')) self.config['firmwareUrl'] = ( conf_dict.get('firmwareUrl', '')) self.config['allowNotify'] = ( conf_dict.get('allowNotify', '')) self.config['deviceImg'] = ( conf_dict.get('deviceImg', '')) self.config['defaultDeviceImg'] = ( conf_dict.get('defaultDeviceImg', '')) self.config['ownerShip'] = ( conf_dict.get('ownerShip', False)) def toggle(self, status: str) -> bool: body = helpers.req_body(self.manager, 'bypassV2') if status == 'off': status_bool = False elif status == 'on': status_bool = True else: logger.debug('Invalid status %s for toggle - only on/off allowed', status) return False if status == self.device_status: logger.debug("Device already in requested state") return True body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'setSwitch', 'source': 'APP', 'data': { 'id': 0, 'enabled': status_bool, } } r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=helpers.req_header_bypass(), json_object=body) if helpers.code_check(r): self.device_status = status return True logger.debug('%s offline', self.device_name) self.device_status = 'off' self.connection_status = 'offline' return False def set_rgb(self, red: NUMERIC_T = None, green: NUMERIC_T = None, blue: NUMERIC_T = None) -> bool: new_color = Color(red=red, green=green, blue=blue).hsv return self.set_hsv(hue=new_color.hue, saturation=new_color.saturation, value=new_color.value) def set_brightness(self, brightness: int) -> bool: """Set brightness of multicolor bulb.""" return self.set_status(brightness=brightness) def set_color_temp(self, color_temp: int) -> bool: """Set White Temperature of Bulb in pct (0 - 100).""" return self.set_status(color_temp=color_temp) def set_color_hue(self, color_hue: float) -> bool: """Set Color Hue of Bulb (0 - 360).""" return self.set_status(color_hue=color_hue) def set_color_saturation(self, color_saturation: float) -> bool: """Set Color Saturation of Bulb in pct (1 - 100).""" return self.set_status(color_saturation=color_saturation) def set_color_value(self, color_value: float) -> bool: """Set Value of multicolor bulb in pct (1 - 100).""" # Equivalent to brightness level, when in color mode. return self.set_status(color_value=color_value) def set_color_mode(self, color_mode: str) -> bool: """Set Color Mode of Bulb (white / hsv).""" return self.set_status(color_mode=color_mode) def set_hsv(self, hue: NUMERIC_T = None, saturation: NUMERIC_T = None, value: NUMERIC_T = None) -> bool: arg_dict = { "hue": self._validate_any(hue, 0, 360, 360) if hue is not None else "", "saturation": self._validate_any( saturation, 0, 100, 100) if saturation is not None else "", "brightness": self._validate_any( value, 0, 100, 100) if value is not None else "" } # the api expects the hsv Value in the brightness parameter if self._color is not None: current_dict = {"hue": self.color_hue, "saturation": self.color_saturation, "brightness": self.color_value} filtered_arg_dict = {k: v for k, v in arg_dict.items() if v != ""} same_colors = all(current_dict.get(k) == v for k, v in filtered_arg_dict.items()) if self.device_status == 'on' and same_colors: logger.debug("Device already in requested state") return True arg_dict = { "hue": int(round(arg_dict["hue"]*27.77778, 0)) if isinstance( arg_dict["hue"], float) else "", "saturation": int(round(arg_dict["saturation"]*100, 0)) if isinstance( arg_dict["saturation"], float) else "", "brightness": int(round(arg_dict["brightness"], 0)) if isinstance( arg_dict["brightness"], float) else "" } arg_dict['colorMode'] = 'hsv' return self._set_status_api(arg_dict) def enable_white_mode(self) -> bool: """Enable white color mode.""" return self.set_status(color_mode='white') def set_status(self, # noqa: C901 *, brightness: NUMERIC_T = None, color_temp: NUMERIC_T = None, color_saturation: NUMERIC_T = None, color_hue: NUMERIC_T = None, color_mode: str | None = None, color_value: NUMERIC_T = None ) -> bool: """Set multicolor bulb parameters. No arguments turns bulb on. **Kwargs only** Args: brightness (int, optional): brightness between 0 and 100 color_temp (int, optional): color temperature between 0 and 100 color_mode (int, optional): color mode hsv or white color_hue (float, optional): color hue between 0 and 360 color_saturation (float, optional): color saturation between 0 and 100 color_value (int, optional): color value between 0 and 100 Returns: bool : True if call was successful, False otherwise """ arg_list = ['brightness', 'color_temp', 'color_saturation', 'color_hue', 'color_mode', 'color_value'] toggle_on = True for val in arg_list: if locals()[val] is not None: toggle_on = False if toggle_on: self.turn_on() # If any HSV color values are passed, # set HSV status & ignore other values # Set Color if hue, saturation or value is set if any(var is not None for var in [color_hue, color_saturation, color_value]): return self.set_hsv(color_hue, color_saturation, color_value) # initiate variables request_dict = { "force": 1, "colorMode": '', "brightness": '', "colorTemp": '', "hue": "", "saturation": "", "value": "" } force_list = ['colorTemp', 'saturation', 'hue', 'colorMode', 'value'] if brightness is not None: brightness_update = self._validate_brightness(brightness) if self.device_status == 'on' and brightness_update == self._brightness: logger.debug('Brightness already set to %s', brightness) return True if all(locals().get(k) is None for k in force_list): request_dict['force'] = 0 request_dict['brightness'] = int(brightness_update) else: brightness_update = None # Set White Temperature of Bulb in pct (1 - 100). if color_temp is not None and \ self._validate_any(color_temp, 0, 100, 100): valid_color_temp = self._validate_any(color_temp, 0, 100, 100) request_dict['colorTemp'] = int(valid_color_temp) request_dict['colorMode'] = 'white' # Set Color Mode of Bulb (white / hsv). if color_mode is not None: possible_modes = {'white': 'white', 'color': 'hsv', 'hsv': 'hsv'} if not isinstance(color_mode, str) or \ color_mode.lower() not in possible_modes: logger.error('Error: invalid color_mode value') return False color_mode = color_mode.lower() request_dict['colorMode'] = possible_modes[color_mode] if self._set_status_api(request_dict) and \ brightness_update is not None: self._brightness = brightness_update return True return False def _set_status_api(self, data_dict: dict) -> bool: """Call api to set status - INTERNAL.""" data_dict_start = { "force": 1, "brightness": '', "colorTemp": "", "colorMode": "", "hue": "", "saturation": "", "value": "" } data_dict_start.update(data_dict) body = helpers.req_body(self.manager, 'bypassV2') body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'setLightStatusV2', 'source': 'APP', 'data': data_dict_start } # Make API call r, _ = helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=helpers.req_header_bypass(), json_object=body, ) # Check result if helpers.code_check(r): self._interpret_apicall_result(r) self.device_status = 'on' return True self.device_status = 'off' self.connection_status = 'offline' logger.debug('%s offline', self.device_name) return False pyvesync-2.1.18/src/pyvesync/vesyncfan.py000066400000000000000000003262371475441503300205230ustar00rootroot00000000000000"""VeSync API for controling fans and purifiers.""" import json import logging from typing import Any, Dict, List, Tuple, Union, Optional from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.helpers import Helpers, Timer humid_features: dict = { 'Classic300S': { 'module': 'VeSyncHumid200300S', 'models': ['Classic300S', 'LUH-A601S-WUSB', 'LUH-A601S-AUSW'], 'features': ['nightlight'], 'mist_modes': ['auto', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)) }, 'Classic200S': { 'module': 'VeSyncHumid200S', 'models': ['Classic200S'], 'features': [], 'mist_modes': ['auto', 'manual'], 'mist_levels': list(range(1, 10)) }, 'Dual200S': { 'module': 'VeSyncHumid200300S', 'models': ['Dual200S', 'LUH-D301S-WUSR', 'LUH-D301S-WJP', 'LUH-D301S-WEU'], 'features': [], 'mist_modes': ['auto', 'manual'], 'mist_levels': list(range(1, 3)) }, 'LV600S': { 'module': 'VeSyncHumid200300S', 'models': ['LUH-A602S-WUSR', 'LUH-A602S-WUS', 'LUH-A602S-WEUR', 'LUH-A602S-WEU', 'LUH-A602S-WJP', 'LUH-A602S-WUSC'], 'features': ['warm_mist', 'nightlight'], 'mist_modes': ['humidity', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)), 'warm_mist_levels': [0, 1, 2, 3] }, 'OASISMISTEU': { 'module': 'VeSyncHumid200300S', 'models': ['LUH-O451S-WEU'], 'features': ['warm_mist', 'nightlight'], 'mist_modes': ['auto', 'manual'], 'mist_levels': list(range(1, 10)), 'warm_mist_levels': list(range(4)) }, 'OASISMIST': { 'module': 'VeSyncHumid200300S', 'models': ['LUH-O451S-WUS', 'LUH-O451S-WUSR', 'LUH-O601S-WUS', 'LUH-O601S-KUS'], 'features': ['warm_mist'], 'mist_modes': ['auto', 'humidity', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)), 'warm_mist_levels': list(range(4)) }, 'OASISMIST1000S': { 'module': 'VeSyncHumid1000S', 'models': ['LUH-M101S-WUS', 'LUH-M101S-WEUR'], 'features': [], 'mist_modes': ['auto', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)) }, 'Superior6000S': { 'module': 'VeSyncSuperior6000S', 'models': ['LEH-S601S-WUS', 'LEH-S601S-WUSR'], 'features': [], 'mist_modes': ['auto', 'humidity', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)) } } air_features: dict = { 'Core200S': { 'module': 'VeSyncAirBypass', 'models': ['Core200S', 'LAP-C201S-AUSR', 'LAP-C202S-WUSR'], 'modes': ['sleep', 'off', 'manual'], 'features': ['reset_filter'], 'levels': list(range(1, 4)) }, 'Core300S': { 'module': 'VeSyncAirBypass', 'models': ['Core300S', 'LAP-C301S-WJP', 'LAP-C302S-WUSB', 'LAP-C301S-WAAA'], 'modes': ['sleep', 'off', 'auto', 'manual'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, 'Core400S': { 'module': 'VeSyncAirBypass', 'models': ['Core400S', 'LAP-C401S-WJP', 'LAP-C401S-WUSR', 'LAP-C401S-WAAA'], 'modes': ['sleep', 'off', 'auto', 'manual'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, 'Core600S': { 'module': 'VeSyncAirBypass', 'models': ['Core600S', 'LAP-C601S-WUS', 'LAP-C601S-WUSR', 'LAP-C601S-WEU'], 'modes': ['sleep', 'off', 'auto', 'manual'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, 'LV-PUR131S': { 'module': 'VeSyncAir131', 'models': ['LV-PUR131S', 'LV-RH131S', 'LV-RH131S-WM'], 'modes': ['manual', 'auto', 'sleep', 'off'], 'features': ['air_quality'], 'levels': list(range(1, 3)) }, 'Vital100S': { 'module': 'VeSyncAirBaseV2', 'models': ['LAP-V102S-AASR', 'LAP-V102S-WUS', 'LAP-V102S-WEU', 'LAP-V102S-AUSR', 'LAP-V102S-WJP'], 'modes': ['manual', 'auto', 'sleep', 'off', 'pet'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, 'Vital200S': { 'module': 'VeSyncAirBaseV2', 'models': ['LAP-V201S-AASR', 'LAP-V201S-WJP', 'LAP-V201S-WEU', 'LAP-V201S-WUS', 'LAP-V201-AUSR', 'LAP-V201S-AUSR', 'LAP-V201S-AEUR'], 'modes': ['manual', 'auto', 'sleep', 'off', 'pet'], 'features': ['air_quality'], 'levels': list(range(1, 5)) }, 'EverestAir': { 'module': 'VeSyncAirBaseV2', 'models': ['LAP-EL551S-AUS', 'LAP-EL551S-AEUR', 'LAP-EL551S-WEU', 'LAP-EL551S-WUS'], 'modes': ['manual', 'auto', 'sleep', 'off', 'turbo'], 'features': ['air_quality', 'fan_rotate'], 'levels': list(range(1, 4)) }, 'SmartTowerFan': { 'module': 'VeSyncTowerFan', 'models': ['LTF-F422S-KEU', 'LTF-F422S-WUSR', 'LTF-F422_WJP', 'LTF-F422S-WUS'], 'modes': ['normal', 'auto', 'advancedSleep', 'turbo', 'off'], 'set_mode_method': 'setTowerFanMode', 'features': ['fan_speed'], 'levels': list(range(1, 13)) } } logger = logging.getLogger(__name__) def model_dict() -> dict: """Build purifier and humidifier model dictionary. Internal function to build a dictionary of device models and their associated classes. Used by the `vesync.object_factory` to determine the class to instantiate. """ model_modules = {} for dev_dict in {**air_features, **humid_features}.values(): for model in dev_dict['models']: model_modules[model] = dev_dict['module'] return model_modules def model_features(dev_type: str) -> dict: """Get features from device type. Used by classes to determine the features of the device. Parameters: dev_type (str): Device model type Returns: dict: Device dictionary Raises: ValueError: Device not configured in `air_features` or `humid_features` """ for dev_dict in {**air_features, **humid_features}.values(): if dev_type in dev_dict['models']: return dev_dict raise ValueError('Device not configured') fan_classes: set = {v['module'] for k, v in {**air_features, **humid_features}.items()} fan_modules: dict = model_dict() __all__: list = list(fan_classes) + ['fan_modules'] class VeSyncAirBypass(VeSyncBaseDevice): """Initialize air purifier devices. Instantiated by VeSync manager object. Inherits from VeSyncBaseDevice class. Parameters: details (dict): Dictionary of device details manager (VeSync): Instantiated VeSync object used to make API calls Attributes: modes (list): List of available operation modes for device air_quality_feature (bool): True if device has air quality sensor details (dict): Dictionary of device details timer (Timer): Timer object for device, None if no timer exists. See [pyveysnc.helpers.Timer][`Timer`] class config (dict): Dictionary of device configuration Notes: The `details` attribute holds device information that is updated when the `update()` method is called. An example of the `details` attribute: ```python >>> json.dumps(self.details, indent=4) { 'filter_life': 0, 'mode': 'manual', 'level': 0, 'display': False, 'child_lock': False, 'night_light': 'off', 'air_quality': 0 # air quality level 'air_quality_value': 0, # PM2.5 value from device, 'display_forever': False } ``` """ def __init__(self, details: Dict[str, list], manager): """Initialize VeSync Air Purifier Bypass Base Class.""" super().__init__(details, manager) self.enabled = True self._config_dict = model_features(self.device_type) self._features = self._config_dict.get('features', []) if not isinstance(self._config_dict.get('modes'), list): logger.error( 'Please set modes for %s in the configuration', self.device_type) raise KeyError(f'Modes not set in configuration for {self.device_name}') self.modes = self._config_dict['modes'] if 'air_quality' in self._features: self.air_quality_feature = True else: self.air_quality_feature = False self.details: Dict[str, Any] = { 'filter_life': 0, 'mode': 'manual', 'level': 0, 'display': False, 'child_lock': False, 'night_light': 'off', } self.timer: Optional[Timer] = None if self.air_quality_feature is True: self.details['air_quality'] = 0 self.config: Dict[str, Union[str, int, float, bool]] = { 'display': False, 'display_forever': False } def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: """Build device api body dictionary. This method is used internally as a helper function to build API requests. Parameters: method (str): API method to call Returns: Tuple(Dict, Dict): Tuple of headers and body dictionaries Notes: Possible methods are: 1. 'getPurifierStatus' 2. 'setSwitch' 3. 'setNightLight' 4. 'setLevel' 5. 'setPurifierMode' 6. 'setDisplay' 7. 'setChildLock' 8. 'setIndicatorLight' 9. 'getTimer' 10. 'addTimer' 11. 'delTimer' 12. 'resetFilter' """ modes = ['getPurifierStatus', 'setSwitch', 'setNightLight', 'setLevel', 'setPurifierMode', 'setDisplay', 'setChildLock', 'setIndicatorLight', 'getTimer', 'addTimer', 'delTimer', 'resetFilter'] 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_purifier_dict(self, dev_dict: dict) -> None: """Build Bypass purifier status dictionary. Populates `self.details` and instance variables with device details. Args: dev_dict (dict): Dictionary of device details from API Examples: >>> dev_dict = { ... 'enabled': True, ... 'filter_life': 0, ... 'mode': 'manual', ... 'level': 0, ... 'display': False, ... 'child_lock': False, ... 'night_light': 'off', ... 'display': False, ... 'display_forever': False, ... 'air_quality_value': 0, ... 'air_quality': 0 ... } >>> build_purifier_dict(dev_dict) >>> print(self.details) { 'filter_life': 0, 'mode': 'manual', 'level': 0, 'display': False, 'child_lock': False, 'night_light': 'off', 'display': False, 'display_forever': False, 'air_quality_value': 0, 'air_quality': 0 } """ self.enabled = dev_dict.get('enabled', False) if self.enabled: self.device_status = 'on' else: self.device_status = 'off' self.details['filter_life'] = dev_dict.get('filter_life', 0) self.mode = dev_dict.get('mode', 'manual') self.speed = dev_dict.get('level', 0) self.details['display'] = dev_dict.get('display', False) self.details['child_lock'] = dev_dict.get('child_lock', False) self.details['night_light'] = dev_dict.get('night_light', 'off') self.details['display_forever'] = dev_dict.get('display_forever', False) if self.air_quality_feature is True: self.details['air_quality_value'] = dev_dict.get( 'air_quality_value', 0) self.details['air_quality'] = dev_dict.get('air_quality', 0) def build_config_dict(self, conf_dict: Dict[str, str]) -> None: """Build configuration dict for Bypass purifier. Used by the `update()` method to populate the `config` attribute. Args: conf_dict (dict): Dictionary of device configuration """ self.config['display'] = conf_dict.get('display', False) self.config['display_forever'] = conf_dict.get('display_forever', False) def get_details(self) -> None: """Build Bypass Purifier 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': 'getPurifierStatus', 'source': 'APP', 'data': {} } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if not isinstance(r, dict): logger.debug('Error in purifier response') 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: 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') else: logger.debug('Error in purifier response') def update(self): """Update Purifier details.""" self.get_details() def get_timer(self) -> Optional[Timer]: """Retrieve running timer from purifier. Returns Timer object if timer is running, None if no timer is running. Args: None Returns: Timer | None : Timer object if timer is running, None if no timer is running Notes: Timer object tracks the time remaining based on the last update. Timer properties include `status`, `time_remaining`, `duration`, `action`, `paused` and `done`. The methods `start()`, `end()` and `pause()` are available but should be called through the purifier object to update through the API. See Also: [pyvesync.helpers.Time][`Timer`] : Timer object used to hold status of timer """ head, body = self.build_api_dict('getTimer') body['payload']['data'] = {} if not head and not body: return None r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if not isinstance(r, dict): logger.debug('Error in purifier response') return None if r.get('code') != 0: logger.debug('Error in purifier response, code %s', r.get('code', 'unknown')) return None if r.get('result', {}).get('code') != 0: logger.debug('Error in purifier result response, code %s', r.get('result', {}).get('code', 'unknown')) return None timers = r.get('result', {}).get('result', {}).get('timers', []) if not isinstance(timers, list) or len(timers) < 1: self.timer = None logger.debug('No timer found') return None timer = timers[0] if self.timer is None: self.timer = Timer( timer_duration=timer.get("duration", timer.get("total", 0)), action=timer.get("action"), id=timer.get("id"), remaining=timer.get("remaining", timer.get("remain")) ) else: self.timer.update(time_remaining=timer.get('remaining')) logger.debug('Timer found: %s', str(self.timer)) return self.timer def set_timer(self, timer_duration: int) -> bool: """Set timer for Purifier. Args: timer_duration (int): Duration of timer in seconds Returns: bool : True if timer is set, False if not """ if self.device_status != 'on': logger.debug("Can't set timer when device is off") head, body = self.build_api_dict('addTimer') if not head and not body: return False body['payload']['data'] = { 'total': timer_duration, 'action': 'off', } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r.get('code') != 0: logger.debug('Error in purifier response, code %s', r.get('code', 'unknown')) return False if r.get('result', {}).get('code') != 0: logger.debug('Error in purifier result response, code %s', r.get('result', {}).get('code', 'unknown')) return False timer_id = r.get('result', {}).get('result', {}).get('id') if timer_id is not None: self.timer = Timer(timer_duration=timer_duration, action='off', id=timer_id) else: self.timer = Timer(timer_duration=timer_duration, action='off') return True def clear_timer(self) -> bool: """Clear timer. Returns True if no error is returned from API call. Returns: bool : True if timer is cleared, False if not """ self.get_timer() if self.timer is None: logger.debug('No timer to clear') return False if self.timer.id is None: logger.debug("Timer doesn't have an ID, can't clear") head, body = self.build_api_dict('delTimer') if not head and not body: return False body['payload']['data'] = { 'id': self.timer.id } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r.get('code') != 0: logger.debug('Error in purifier response, code %s', r.get('code', 'unknown')) return False logger.debug("Timer cleared") return True def change_fan_speed(self, speed=None) -> bool: """Change fan speed based on levels in configuration dict. If no value is passed, the next speed in the list is selected. Args: speed (int, optional): Speed to set fan. Defaults to None. Returns: bool : True if speed is set, False if not """ speeds: list = self._config_dict.get('levels', []) current_speed = self.speed if speed is not None: if speed not in speeds: logger.debug("%s is invalid speed - valid speeds are %s", speed, str(speeds)) return False new_speed = speed else: if current_speed == speeds[-1]: new_speed = speeds[0] else: current_index = speeds.index(current_speed) new_speed = speeds[current_index + 1] body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid head, body = self.build_api_dict('setLevel') if not head and not body: return False body['payload']['data'] = { 'id': 0, 'level': new_speed, 'type': 'wind', 'mode': 'manual', } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.speed = new_speed return True logger.debug('Error changing %s speed', self.device_name) return False def child_lock_on(self) -> bool: """Turn Bypass child lock on.""" return self.set_child_lock(True) def child_lock_off(self) -> bool: """Turn Bypass child lock off. Returns: bool : True if child lock is turned off, False if not """ return self.set_child_lock(False) def set_child_lock(self, mode: bool) -> bool: """Set Bypass child lock. Set child lock to on or off. Internal method used by `child_lock_on` and `child_lock_off`. Args: mode (bool): True to turn child lock on, False to turn off Returns: bool : True if child lock is set, False if not """ if mode not in (True, False): logger.debug('Invalid mode passed to set_child_lock - %s', mode) return False head, body = self.build_api_dict('setChildLock') if not head and not body: return False body['payload']['data'] = { 'child_lock': mode } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.details['child_lock'] = mode return True if isinstance(r, dict): logger.debug('Error toggling child lock') else: logger.debug('Error in api return json for %s', self.device_name) return False def reset_filter(self) -> bool: """Reset filter to 100%. Returns: bool : True if filter is reset, False if not """ if 'reset_filter' not in self._features: logger.debug("Filter reset not implemented for %s", self.device_type) return False head, body = self.build_api_dict('resetFilter') if not head and not body: return False body['payload']['data'] = {} r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error resetting filter') return False def mode_toggle(self, mode: str) -> bool: """Set purifier mode - sleep or manual. Set purifier mode based on devices available modes. Args: mode (str): Mode to set purifier. Based on device modes in attribute `modes` Returns: bool : True if mode is set, False if not """ if mode.lower() not in self.modes: logger.debug('Invalid purifier mode used - %s', mode) return False head, body = self.build_api_dict('setPurifierMode') if not head and not body: return False body['payload']['data'] = { 'mode': mode.lower() } if mode == 'manual': body['payload'] = { 'data': { 'id': 0, 'level': 1, 'type': 'wind' }, 'method': 'setLevel', 'type': 'APP' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if Helpers.code_check(r): if mode.lower() == 'manual': self.speed = 1 self.mode = 'manual' else: self.mode = mode self.speed = 0 return True logger.debug('Error setting purifier mode') return False def manual_mode(self) -> bool: """Set mode to manual. Calls method [pyvesync.VeSyncAirBypass.mode_toggle][`self.mode_toggle('manual')`] to set mode to manual. Returns: bool : True if mode is set, False if not """ if 'manual' not in self.modes: logger.debug('%s does not have manual mode', self.device_name) return False return self.mode_toggle('manual') def sleep_mode(self) -> bool: """Set sleep mode to on. Calls method [pyvesync.VeSyncAirBypass.mode_toggle][`self.mode_toggle('sleep')`] Returns: bool : True if mode is set, False if not """ if 'sleep' not in self.modes: logger.debug('%s does not have sleep mode', self.device_name) return False return self.mode_toggle('sleep') def auto_mode(self) -> bool: """Set mode to auto. Calls method [pyvesync.VeSyncAirBypass.mode_toggle][`self.mode_toggle('sleep')`] Returns: bool : True if mode is set, False if not """ if 'auto' not in self.modes: logger.debug('%s does not have auto mode', self.device_name) return False return self.mode_toggle('auto') def toggle_switch(self, toggle: bool) -> bool: """Toggle purifier on/off. Helper method for `turn_on()` and `turn_off()` methods. Args: toggle (bool): True to turn on, False to turn off Returns: bool : True if purifier is toggled, False if not """ if not isinstance(toggle, bool): logger.debug('Invalid toggle value for purifier 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_object=body, ) if r is not None and Helpers.code_check(r): if toggle: self.device_status = 'on' else: self.device_status = 'off' return True logger.debug("Error toggling purifier - %s", self.device_name) return False def turn_on(self) -> bool: """Turn bypass Purifier on. Calls method [pyvesync.VeSyncAirBypass.toggle_switch][`self.toggle_switch(True)`] Returns: bool : True if purifier is turned on, False if not """ return self.toggle_switch(True) def turn_off(self): """Turn Bypass Purifier off. Calls method [pyvesync.VeSyncAirBypass.toggle_switch][`self.toggle_switch(False)`] Returns: bool : True if purifier is turned off, False if not """ return self.toggle_switch(False) def set_display(self, mode: bool) -> bool: """Toggle display on/off. Called by `turn_on_display()` and `turn_off_display()` methods. Args: mode (bool): True to turn display on, False to turn off Returns: bool : True if display is toggled, False if not """ 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_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug("Error toggling purifier display - %s", self.device_name) return False def turn_on_display(self) -> bool: """Turn Display on. Calls method [pyvesync.VeSyncAirBypass.set_display][`self.set_display(True)`] Returns: bool : True if display is turned on, False if not """ return self.set_display(True) def turn_off_display(self): """Turn Display off. Calls method [pyvesync.VeSyncAirBypass.set_display][`self.set_display(False)`] Returns: bool : True if display is turned off, False if not """ return self.set_display(False) def set_night_light(self, mode: str) -> bool: """Set night light. Possible modes are on, off or dim. Args: mode (str): Mode to set night light Returns: bool : True if night light is set, False if not """ if mode.lower() not in ['on', 'off', 'dim']: logger.debug('Invalid nightlight mode used (on, off or dim)- %s', mode) return False head, body = self.build_api_dict('setNightLight') if not head and not body: return False body['payload']['data'] = { 'night_light': mode.lower() } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.details['night_light'] = mode.lower() return True logger.debug('Error setting nightlight mode') return False @property def air_quality(self): """Get air quality value (ug/m3).""" if self.air_quality_feature is not True: logger.debug("%s does not have air quality sensor", self.device_type) try: return int(self.details['air_quality']) except KeyError: return 0 @property def fan_level(self): """Get current fan level.""" try: speed = int(self.speed) except ValueError: speed = self.speed return speed @property def filter_life(self) -> int: """Get percentage of filter life remaining.""" try: return int(self.details['filter_life']) except KeyError: return 0 @property def display_state(self) -> bool: """Get display state. See [pyvesync.VeSyncAirBypass.display_status][`self.display_status`] """ return bool(self.details['display']) @property def screen_status(self) -> bool: """Get display status. Returns: bool : True if display is on, False if off """ return bool(self.details['display']) @property def child_lock(self) -> bool: """Get child lock state. Returns: bool : True if child lock is enabled, False if not. """ return bool(self.details['child_lock']) @property def night_light(self) -> str: """Get night light state. Returns: str : Night light state (on, dim, off) """ return str(self.details['night_light']) def display(self) -> None: """Print formatted device info to stdout. Builds on the `display()` method from the `VeSyncBaseDevice` class. See Also: [pyvesync.VeSyncBaseDevice.display][`VeSyncBaseDevice.display`] """ super().display() disp = [ ('Mode: ', self.mode, ''), ('Filter Life: ', self.details['filter_life'], 'percent'), ('Fan Level: ', self.speed, ''), ('Display: ', self.details['display'], ''), ('Child Lock: ', self.details['child_lock'], ''), ('Night Light: ', self.details['night_light'], ''), ('Display Config: ', self.config['display'], ''), ('Display_Forever Config: ', self.config['display_forever'], '') ] if self.air_quality_feature: disp.extend([ ('Air Quality Level: ', self.details.get('air_quality', ''), ''), ('Air Quality Value: ', self.details.get('air_quality_value', ''), 'ug/m3') ]) for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') def displayJSON(self) -> str: # noqa: N802 """Return air purifier status and properties in JSON output. Returns: str : JSON formatted string of air purifier details """ 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': self.details['display'], 'Child Lock': self.details['child_lock'], 'Night Light': str(self.details['night_light']), 'Display Config': self.config['display'], 'Display_Forever Config': self.config['display_forever'], } ) 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 VeSyncAirBaseV2(VeSyncAirBypass): """Levoit V2 Air Purifier Class. Inherits from VeSyncAirBypass and VeSyncBaseDevice class. Args: details (dict): Dictionary of device details manager (VeSync): Instantiated VeSync object Attributes: set_speed_level (int): Set speed level for device auto_prefences (list): List of auto preferences for device modes (list): List of available operation modes for device air_quality_feature (bool): True if device has air quality sensor details (dict): Dictionary of device details timer (Timer): Timer object for device, None if no timer exists. See [pyveysnc.helpers.Timer][`Timer`] class config (dict): Dictionary of device configuration """ def __init__(self, details: Dict[str, list], manager): """Initialize the VeSync Base API V2 Air Purifier Class.""" super().__init__(details, manager) self.set_speed_level: Optional[int] = None self.auto_prefences: List[str] = ['default', 'efficient', 'quiet'] def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: """Return default body for Bypass V2 API.""" header = Helpers.bypass_header() body = Helpers.bypass_body_v2(self.manager) body['cid'] = self.cid body['deviceId'] = self.cid body['configModule'] = self.config_module body['configModel'] = self.config_module body['payload'] = { 'method': method, 'source': 'APP', 'data': {} } return header, body @property def light_detection(self) -> bool: """Return true if light detection feature is enabled.""" return self.details['light_detection_switch'] @light_detection.setter def light_detection(self, toggle: bool) -> None: """Set light detection feature.""" self.details['light_detection_switch'] = toggle @property def light_detection_state(self) -> bool: """Return true if light is detected.""" return self.details['environment_light_state'] @property def fan_level(self): """Get current fan level.""" try: speed = int(self.set_speed_level) except ValueError: speed = self.set_speed_level return speed def get_details(self) -> None: """Build API V2 Purifier details dictionary.""" head, body = self.build_api_dict('getPurifierStatus') r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if Helpers.nested_code_check(r) is False or not isinstance(r, dict): logger.debug('Error getting purifier details') self.connection_status = 'offline' return inner_result = r.get('result', {}).get('result') if inner_result is not None: self.build_purifier_dict(inner_result) else: 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_purifier_dict(self, dev_dict: dict) -> None: """Build Bypass purifier status dictionary.""" 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' self.mode = dev_dict.get('workMode', 'manual') self.speed = dev_dict.get('fanSpeedLevel', 0) self.set_speed_level = dev_dict.get('manualSpeedLevel', 1) self.details['filter_life'] = dev_dict.get('filterLifePercent', 0) self.details['child_lock'] = bool(dev_dict.get('childLockSwitch', 0)) self.details['display'] = bool(dev_dict.get('screenState', 0)) 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_switch'] = bool(dev_dict.get('screenSwitch', 0)) if self.air_quality_feature is True: self.details['air_quality_value'] = dev_dict.get( 'PM25', 0) self.details['air_quality'] = dev_dict.get('AQLevel', 0) if 'PM1' in dev_dict: self.details['pm1'] = dev_dict['PM1'] if 'PM10' in dev_dict: self.details['pm10'] = dev_dict['PM10'] if 'AQPercent' in dev_dict: self.details['aq_percent'] = dev_dict['AQPercent'] if 'fanRotateAngle' in dev_dict: self.details['fan_rotate_angle'] = dev_dict['fanRotateAngle'] if 'filterOpenState' in dev_dict: self.details['filter_open_state'] = bool(dev_dict['filterOpenState']) if dev_dict.get('timerRemain', 0) > 0: self.timer = Timer(dev_dict['timerRemain'], 'off') if isinstance(dev_dict.get('autoPreference'), dict): self.details['auto_preference_type'] = dev_dict.get( 'autoPreference', {}).get('autoPreferenceType', 'default') else: self.details['auto_preference_type'] = None def turbo_mode(self) -> bool: """Turn on Turbo mode for compatible devices.""" if 'turbo' in self.modes: return self.mode_toggle('turbo') logger.debug("Turbo mode not available for %s", self.device_name) return False def pet_mode(self) -> bool: """Set Pet Mode for compatible devices.""" if 'pet' in self.modes: return self.mode_toggle('pet') logger.debug("Pet mode not available for %s", self.device_name) return False def set_night_light(self, mode: str) -> bool: """TODO: Set night light.""" logger.debug("Night light feature not configured") return False def set_light_detection(self, toggle: bool) -> bool: """Enable/Disable Light Detection Feature.""" 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 head, body = self.build_api_dict('setLightDetection') body['payload']['data']['lightDetectionSwitch'] = toggle_id r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): self.details['light_detection'] = toggle return True logger.debug("Error toggling purifier - %s", self.device_name) return False def set_light_detection_on(self) -> bool: """Turn on light detection feature.""" return self.set_light_detection(True) def set_light_detection_off(self) -> bool: """Turn off light detection feature.""" return self.set_light_detection(False) def toggle_switch(self, toggle: bool) -> bool: """Toggle purifier on/off.""" if not isinstance(toggle, bool): logger.debug('Invalid toggle value for purifier switch') return False head, body = self.build_api_dict('setSwitch') power = int(toggle) body['payload']['data'] = { 'powerSwitch': power, 'switchIdx': 0 } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): if toggle is True: self.device_status = 'on' else: self.device_status = 'off' return True logger.debug("Error toggling purifier - %s", self.device_name) return False def set_child_lock(self, mode: bool) -> bool: """Levoit 100S/200S set Child Lock. Parameters: mode (bool): True to turn child lock on, False to turn off Returns: bool : True if successful, False if not """ toggle_id = int(mode) head, body = self.build_api_dict('setChildLock') body['payload']['data'] = { 'childLockSwitch': toggle_id } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): self.details['child_lock'] = mode return True logger.debug("Error toggling purifier child lock - %s", self.device_name) return False def set_display(self, mode: bool) -> bool: """Levoit Vital 100S/200S Set Display on/off with True/False.""" mode_id = int(mode) head, body = self.build_api_dict('setDisplay') body['payload']['data'] = { 'screenSwitch': mode_id } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): self.details['screen_switch'] = mode return True logger.debug("Error toggling purifier display - %s", self.device_name) return False def set_timer(self, timer_duration: int, action: str = 'off', method: str = 'powerSwitch') -> bool: """Set timer for Levoit 100S. Parameters: timer_duration (int): Timer duration in seconds. action (str | None): Action to perform, on or off, by default 'off' method (str | None): Method to use, by default 'powerSwitch' - TODO: Implement other methods Returns: bool : True if successful, False if not """ if action not in ['on', 'off']: logger.debug('Invalid action for timer') return False if method not in ['powerSwitch']: logger.debug('Invalid method for timer') return False action_id = 1 if action == 'on' else 0 head, body = self.build_api_dict('addTimerV2') body['payload']['subDeviceNo'] = 0 body['payload']['data'] = { "startAct": [{ "type": method, "num": 0, "act": action_id, }], "total": timer_duration, "subDeviceNo": 0 } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): self.timer = Timer(timer_duration, action) return True logger.debug("Error setting timer for - %s", self.device_name) return False def clear_timer(self) -> bool: """Clear running timer.""" head, body = self.build_api_dict('delTimerV2') body['payload']['subDeviceNo'] = 0 body['payload']['data'] = {'id': 1, "subDeviceNo": 0} r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.nested_code_check(r): self.timer = None return True logger.debug("Error setting timer for - %s", self.device_name) return False def set_auto_preference(self, preference: str = 'default', room_size: int = 600) -> bool: """Set Levoit Vital 100S/200S auto mode. Parameters: preference (str | None): Preference for auto mode, default 'default' (default, efficient, quiet) room_size (int | None): Room size in square feet, by default 600 """ if preference not in self.auto_prefences: logger.debug("%s is invalid preference -" " valid preferences are default, efficient, quiet", preference) return False head, body = self.build_api_dict('setAutoPreference') body['payload']['data'] = { 'autoPreference': preference, 'roomSize': room_size, } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.details['auto_preference'] = preference return True logger.debug("Error setting auto preference for - %s", self.device_name) return False def change_fan_speed(self, speed=None) -> bool: """Change fan speed based on levels in configuration dict. The levels are defined in the configuration dict for the device. If no level is passed, the next valid level will be used. If the current level is the last level. Parameters: speed (int | None): Speed to set based on levels in configuration dict """ speeds: list = self._config_dict.get('levels', []) current_speed = self.set_speed_level or 0 if speed is not None: if speed not in speeds: logger.debug("%s is invalid speed - valid speeds are %s", speed, str(speeds)) return False new_speed = speed else: if current_speed in [speeds[-1], 0]: new_speed = speeds[0] else: current_index = speeds.index(current_speed) new_speed = speeds[current_index + 1] head, body = self.build_api_dict('setLevel') if not head or not body: return False body['payload']['data'] = { 'levelIdx': 0, 'manualSpeedLevel': new_speed, 'levelType': 'wind' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.set_speed_level = new_speed self.mode = 'manual' return True logger.debug('Error changing %s speed', self.device_name) return False def mode_toggle(self, mode: str) -> bool: """Set Levoit 100S purifier mode. Parameters: mode (str): Mode to set purifier to, options are: auto, manual, sleep Returns: bool : True if successful, False if not """ if mode.lower() not in self.modes: logger.debug('Invalid purifier mode used - %s', mode) return False # Call change_fan_speed if mode is set to manual if mode == 'manual': if self.speed is None or self.speed == 0: return self.change_fan_speed(1) return self.change_fan_speed(self.speed) if mode == 'off': return self.turn_off() head, body = self.build_api_dict('setPurifierMode') if not head and not body: return False body['deviceId'] = self.cid body['payload']['data'] = { 'workMode': mode } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if Helpers.code_check(r): self.mode = mode return True 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', ''))} ) everest_keys = { 'pm1': 'PM1', 'pm10': 'PM10', 'fan_rotate_angle': 'Fan Rotate Angle', 'filter_open_state': 'Filter Open State' } for key, value in everest_keys.items(): if key in self.details: sup_val.update({value: str(self.details[key])}) return json.dumps(sup_val, indent=4) class VeSyncAir131(VeSyncBaseDevice): """Levoit Air Purifier Class.""" def __init__(self, details, manager): """Initilize air purifier class.""" super().__init__(details, manager) self.enabled = True self._config_dict = model_features(self.device_type) self._features = self._config_dict.get('features', []) if not isinstance(self._config_dict.get('modes'), list): logger.error( 'Please set modes for %s in the configuration', self.device_type) raise KeyError(f'Modes not set in configuration for {self.device_name}') self.modes = self._config_dict['modes'] if 'air_quality' in self._features: self.air_quality_feature = True else: self.air_quality_feature = False self.details = {} def get_details(self) -> None: """Build Air Purifier details dictionary.""" body = Helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid head = Helpers.req_headers(self.manager) r, _ = Helpers.call_api( '/131airPurifier/v1/device/deviceDetail', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): self.device_status = r.get('deviceStatus', 'unknown') self.connection_status = r.get('connectionStatus', 'unknown') self.details['active_time'] = r.get('activeTime', 0) self.details['filter_life'] = r.get('filterLife', {}) self.details['screen_status'] = r.get('screenStatus', 'unknown') self.mode = r.get('mode', self.mode) self.details['level'] = r.get('level', 0) self.details['air_quality'] = r.get('airQuality', 'unknown') else: logger.debug('Error getting %s details', self.device_name) def get_config(self) -> None: """Get configuration info for air purifier.""" body = Helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = Helpers.call_api( '/131airpurifier/v1/device/configurations', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if r is not None and Helpers.code_check(r): self.config = Helpers.build_config_dict(r) else: logger.debug('Unable to get config info for %s', self.device_name) @property def active_time(self) -> int: """Return total time active in minutes.""" return self.details.get('active_time', 0) @property def fan_level(self) -> int: """Get current fan level (1-3).""" return self.details.get('level', 0) @property def filter_life(self) -> int: """Get percentage of filter life remaining.""" try: return self.details['filter_life'].get('percent', 0) except KeyError: return 0 @property def air_quality(self) -> str: """Get Air Quality.""" return self.details.get('air_quality', 'unknown') @property def display_state(self) -> bool: """Get display state. See [pyvesync.VeSyncAir131.get_details] """ return self.details.get('screen_status', 'unknown') == "on" @property def screen_status(self) -> str: """Return Screen status (on/off).""" return self.details.get('screen_status', 'unknown') def turn_on_display(self) -> bool: """Turn display on.""" return self.toggle_display('on') def turn_off_display(self) -> bool: """Turn display off.""" return self.toggle_display('off') def toggle_display(self, status: str) -> bool: """Toggle Display of VeSync LV-PUR131.""" if status.lower() not in ['on', 'off']: logger.debug('Invalid display status - %s', status) return False head = Helpers.req_headers(self.manager) body = Helpers.req_body(self.manager, 'devicestatus') body['status'] = status.lower() r, _ = Helpers.call_api( '/131airPurifier/v1/device/updateScreen', 'put', json_object=body, headers=head ) if r is not None and Helpers.code_check(r): self.details['screen_status'] = status.lower() return True logger.debug('Error toggling display for %s', self.device_name) return False def turn_on(self) -> bool: """Turn Air Purifier on.""" if self.device_status != 'on': body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'on' head = Helpers.req_headers(self.manager) r, _ = Helpers.call_api( '/131airPurifier/v1/device/deviceStatus', 'put', json_object=body, headers=head ) if r is not None and Helpers.code_check(r): self.device_status = 'on' return True logger.debug('Error turning %s on', self.device_name) return False return False def turn_off(self) -> bool: """Turn Air Purifier Off.""" if self.device_status == 'on': body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'off' head = Helpers.req_headers(self.manager) r, _ = Helpers.call_api( '/131airPurifier/v1/device/deviceStatus', 'put', json_object=body, headers=head ) if r is not None and Helpers.code_check(r): self.device_status = 'off' return True logger.debug('Error turning %s off', self.device_name) return False return True def auto_mode(self) -> bool: """Set mode to auto.""" return self.mode_toggle('auto') def manual_mode(self) -> bool: """Set mode to manual.""" return self.mode_toggle('manual') def sleep_mode(self) -> bool: """Set sleep mode to on.""" return self.mode_toggle('sleep') def change_fan_speed(self, speed: Optional[int] = None) -> bool: """Adjust Fan Speed for air purifier. Specifying 1,2,3 as argument or call without argument to cycle through speeds increasing by one. """ if self.mode != 'manual': logger.debug('%s not in manual mode, cannot change speed', self.device_name) return False try: level = self.details['level'] except KeyError: logger.debug( 'Cannot change fan speed, no level set for %s', self.device_name ) return False body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid head = Helpers.req_headers(self.manager) if speed is not None: if speed == level: return True if speed in [1, 2, 3]: body['level'] = speed else: logger.debug('Invalid fan speed for %s', self.device_name) return False else: if (level + 1) > 3: body['level'] = 1 else: body['level'] = int(level + 1) r, _ = Helpers.call_api( '/131airPurifier/v1/device/updateSpeed', 'put', json_object=body, headers=head ) if r is not None and Helpers.code_check(r): self.details['level'] = body['level'] return True logger.debug('Error changing %s speed', self.device_name) return False def mode_toggle(self, mode: str) -> bool: """Set mode to manual, auto or sleep.""" head = Helpers.req_headers(self.manager) body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid if mode != self.mode and mode in ['sleep', 'auto', 'manual']: body['mode'] = mode if mode == 'manual': body['level'] = 1 r, _ = Helpers.call_api( '/131airPurifier/v1/device/updateMode', 'put', json_object=body, headers=head ) if r is not None and Helpers.code_check(r): self.mode = mode return True logger.debug('Error setting %s mode - %s', self.device_name, mode) return False def update(self) -> None: """Run function to get device details.""" self.get_details() def display(self) -> None: """Return formatted device info to stdout.""" super().display() disp = [ ('Active Time : ', self.active_time, ' minutes'), ('Fan Level: ', self.fan_level, ''), ('Air Quality: ', self.air_quality, ''), ('Mode: ', self.mode, ''), ('Screen Status: ', self.screen_status, ''), ('Filter Life: ', json.dumps(self.filter_life), ' percent') ] for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') def displayJSON(self) -> str: """Return air purifier status and properties in JSON output.""" sup = super().displayJSON() sup_val = json.loads(sup) sup_val.update( { 'Active Time': str(self.active_time), 'Fan Level': self.fan_level, 'Air Quality': self.air_quality, 'Mode': self.mode, 'Screen Status': self.screen_status, 'Filter Life': str(self.filter_life) } ) return json.dumps(sup_val, indent=4) class VeSyncTowerFan(VeSyncAirBaseV2): """Levoit Tower Fan Device Class.""" def __init__(self, details: Dict[str, list], manager): """Initialize the VeSync Base API V2 Fan Class.""" super().__init__(details, manager) def get_details(self) -> None: """Build API V2 Fan details dictionary.""" head, body = self.build_api_dict('getTowerFanStatus') r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if Helpers.nested_code_check(r) is False or not isinstance(r, dict): logger.debug('Error getting purifier details') self.connection_status = 'offline' return inner_result = r.get('result', {}).get('result') if inner_result is not None: self.build_purifier_dict(inner_result) else: 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 mode_toggle(self, mode: str) -> bool: """Set Levoit Tower Fan purifier mode. Parameters: mode : str Mode to set purifier to, set by `config_dict` Returns: bool : True if successful, False if not """ if mode.lower() not in [x.lower() for x in self.modes]: logger.debug('Invalid purifier mode used - %s', mode) return False if mode == 'off': return self.turn_off() head, body = self.build_api_dict('setTowerFanMode') if not head and not body: return False body['deviceId'] = self.cid body['payload']['data'] = { 'workMode': mode } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if Helpers.code_check(r): self.mode = mode return True logger.debug('Error setting purifier mode') return False def normal_mode(self): """Set mode to normal.""" return self.mode_toggle('normal') def manual_mode(self): """Adapter to set mode to normal.""" return self.normal_mode() def advanced_sleep_mode(self) -> bool: """Set advanced sleep mode.""" return self.mode_toggle('advancedSleep') def sleep_mode(self) -> bool: """Adapter to set advanced sleep mode.""" return self.advanced_sleep_mode() class VeSyncHumid200300S(VeSyncBaseDevice): """200S/300S Humidifier Class.""" def __init__(self, details, manager): """Initialize 200S/300S Humidifier class.""" super().__init__(details, manager) self.enabled = True self._config_dict = model_features(self.device_type) self.mist_levels = self._config_dict.get('mist_levels') self.mist_modes = self._config_dict.get('mist_modes') self._features = self._config_dict.get('features') if 'warm_mist' in self._features: self.warm_mist_levels = self._config_dict.get( 'warm_mist_levels', []) self.warm_mist_feature = True else: self.warm_mist_feature = False self.warm_mist_levels = [] if 'nightlight' in self._config_dict.get('features', []): self.night_light = True else: self.night_light = False self.details = { '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, } if self.night_light is True: self.details['night_light_brightness'] = 0 self.config = { 'auto_target_humidity': 0, 'display': False, 'automatic_stop': True } self._api_modes = ['getHumidifierStatus', 'setAutomaticStop', 'setSwitch', 'setNightLightBrightness', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setDisplay', 'setLevel'] def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: """Build humidifier api call header and body. Available methods are: 'getHumidifierStatus', 'setAutomaticStop', 'setSwitch', 'setNightLightBrightness', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode' """ if method not in self._api_modes: logger.debug('Invalid mode - %s', method) raise ValueError 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[str, str]) -> None: """Build humidifier status dictionary.""" self.enabled = dev_dict.get('enabled') self.device_status = 'on' if self.enabled else 'off' self.mode = dev_dict.get('mode', None) 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['automatic_stop_reach_target'] = dev_dict.get( 'automatic_stop_reach_target', True ) if self.night_light: self.details['night_light_brightness'] = dev_dict.get( 'night_light_brightness', 0) if self.warm_mist_feature: self.details['warm_mist_level'] = dev_dict.get( 'warm_level', 0) self.details['warm_mist_enabled'] = dev_dict.get( 'warm_enabled', False) try: self.details['display'] = dev_dict['display'] except KeyError: self.details['display'] = dev_dict.get( 'indicator_light_switch', False) 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/300S 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_object=body, ) if r is None or not isinstance(r, dict): logger.debug("Error getting status of %s ", self.device_name) return 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/300S 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_object=body, ) if r is not None and Helpers.code_check(r): if toggle: self.device_status = 'on' else: self.device_status = 'off' return True logger.debug("Error toggling 300S humidifier - %s", self.device_name) return False def turn_on(self) -> bool: """Turn 200S/300S Humidifier on.""" return self.toggle_switch(True) def turn_off(self): """Turn 200S/300S Humidifier off.""" return self.toggle_switch(False) def automatic_stop_on(self) -> bool: """Turn 200S/300S Humidifier automatic stop on.""" return self.set_automatic_stop(True) def automatic_stop_off(self) -> bool: """Turn 200S/300S Humidifier automatic stop on.""" return self.set_automatic_stop(False) def set_automatic_stop(self, mode: bool) -> bool: """Set 200S/300S 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_object=body, ) if r is not None and 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_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug("Error toggling 300S display - %s", self.device_name) return False def turn_on_display(self) -> bool: """Turn 200S/300S Humidifier on.""" return self.set_display(True) def turn_off_display(self): """Turn 200S/300S Humidifier off.""" return self.set_display(False) @property def display_state(self) -> bool: """Get display state.""" return bool(self.details['display']) def set_humidity(self, humidity: int) -> bool: """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 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_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity') return False def set_night_light_brightness(self, brightness: int) -> bool: """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 head, body = self.build_api_dict('setNightLightBrightness') if not head and not body: return False body['payload']['data'] = { 'night_light_brightness': brightness } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting night light brightness') return False def set_humidity_mode(self, mode: str) -> bool: """Set humidifier mode - sleep or auto.""" if mode.lower() not in self.mist_modes: logger.debug('Invalid humidity mode used - %s', mode) logger.debug('Proper modes for this device are - %s', str(self.mist_modes)) 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_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity mode') return False def set_warm_level(self, warm_level) -> bool: """Set target 600S Humidifier mist warmth.""" if not self.warm_mist_feature: logger.debug('%s is a %s does not have a mist warmer', self.device_name, self.device_type) return False if not isinstance(warm_level, int): try: warm_level = int(warm_level) except ValueError: logger.debug('Error converting warm mist level to a integer') if warm_level not in self.warm_mist_levels: logger.debug("warm_level value must be - %s", str(self.warm_mist_levels)) return False head, body = self.build_api_dict('setLevel') if not head and not body: return False body['payload']['data'] = { 'type': 'warm', 'level': warm_level, 'id': 0, } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting warm') return False def set_auto_mode(self): """Set auto mode for humidifiers.""" if 'auto' in self.mist_modes: call_str = 'auto' elif 'humidity' in self.mist_modes: call_str = 'humidity' else: logger.debug('Trying auto mode, mode not set for this model, ' 'please ensure %s model ' 'is in configuration dictionary', self.device_type) call_str = 'auto' set_auto = self.set_humidity_mode(call_str) return set_auto def set_manual_mode(self): """Set humifier to manual mode with 1 mist level.""" return self.set_humidity_mode('manual') def set_mist_level(self, level) -> bool: """Set humidifier mist level with int between 0 - 9.""" try: level = int(level) except ValueError: level = str(level) if level not in self.mist_levels: 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_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting mist level') return False @property def humidity(self): """Get Humidity level.""" return self.details['humidity'] @property def mist_level(self): """Get current mist level.""" return self.details['mist_virtual_level'] @property def water_lacks(self): """If tank is empty return true.""" return self.details['water_lacks'] @property def auto_humidity(self): """Auto target humidity.""" return self.config['auto_target_humidity'] @property def auto_enabled(self): """Auto mode is enabled.""" if self.details.get('mode') == 'auto' \ or self.details.get('mode') == 'humidity': return True return False @property def warm_mist_enabled(self): """Warm mist feature enabled.""" if self.warm_mist_feature: return self.details['warm_mist_enabled'] return False def display(self) -> None: """Return formatted device info to stdout.""" super().display() disp = [ ('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'], ''), ] if self.night_light: disp.append(('Night Light Brightness: ', self.details.get('night_light_brightness', ''), 'percent')) if self.warm_mist_feature: disp.append(('Warm mist enabled: ', self.details.get('warm_mist_enabled', ''), '')) disp.append(('Warm mist level: ', self.details.get('warm_mist_level', ''), '')) for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') 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'], 'Auto Target Humidity': str(self.config[ 'auto_target_humidity']), 'Automatic Stop': self.config['automatic_stop'], } ) if self.night_light: sup_val['Night Light Brightness'] = self.details[ 'night_light_brightness'] if self.warm_mist_feature: sup_val['Warm mist enabled'] = self.details['warm_mist_enabled'] sup_val['Warm mist level'] = self.details['warm_mist_level'] return json.dumps(sup_val, indent=4) class VeSyncHumid200S(VeSyncHumid200300S): """Levoit Classic 200S Specific class.""" def __init__(self, details, manager): """Initialize levoit 200S device class.""" super().__init__(details, manager) self._api_modes = ['getHumidifierStatus', 'setAutomaticStop', 'setSwitch', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setIndicatorLightSwitch'] 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('setIndicatorLightSwitch') body['payload']['data'] = { 'enabled': mode, 'id': 0 } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug("Error toggling 200S display - %s", self.device_name) return False class VeSyncSuperior6000S(VeSyncBaseDevice): """Superior 6000S Humidifier.""" def __init__(self, details, manager): """Initialize Superior 6000S Humidifier class.""" super().__init__(details, manager) self._config_dict = model_features(self.device_type) self.mist_levels = self._config_dict.get('mist_levels') self.mist_modes = self._config_dict.get('mist_modes') self.connection_status = details.get('deviceProp', {}).get( 'connectionStatus', None) self.details = {} self.config = {} self._api_modes = [ 'getHumidifierStatus', 'setSwitch', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setDisplay', 'setDryingMode', ] def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: """Build humidifier api call header and body. Available methods are: 'getHumidifierStatus', 'setSwitch', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setDisplay', 'setDryingMode' """ if method not in self._api_modes: logger.debug('Invalid mode - %s', method) raise ValueError 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[str, str]) -> None: """Build humidifier status dictionary.""" self.device_status = 'off' if dev_dict.get('powerSwitch', 0) == 0 else 'on' self.mode = 'auto' if dev_dict.get('workMode', '') == 'autoPro' \ else dev_dict.get('workMode', '') self.details['humidity'] = dev_dict.get('humidity', 0) self.details['target_humidity'] = dev_dict.get('targetHumidity', None) self.details['mist_virtual_level'] = dev_dict.get( 'virtualLevel', 0) self.details['mist_level'] = dev_dict.get('mistLevel', 0) self.details['water_lacks'] = dev_dict.get('waterLacksState', False) self.details['water_tank_lifted'] = dev_dict.get( 'waterTankLifted', False) self.details['filter_life_percentage'] = dev_dict.get('filterLifePercent', 0) self.details['temperature'] = dev_dict.get('temperature', 0) self.details['display'] = dev_dict.get('screenSwitch', None) self.details['drying_mode'] = dev_dict.get('dryingMode', {}) def build_config_dict(self, _): """Build configuration dict for humidifier.""" def get_details(self) -> None: """Build 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_object=body, ) if r is None or not isinstance(r, dict): logger.debug("Error getting status of %s ", self.device_name) return 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 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': { 'powerSwitch': int(toggle), 'switchIdx': 0 }, 'method': 'setSwitch', 'source': 'APP' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): if toggle: self.device_status = 'on' else: self.device_status = 'off' return True logger.debug("Error toggling humidifier - %s", self.device_name) return False def turn_on(self) -> bool: """Turn humidifier on.""" return self.toggle_switch(True) def turn_off(self): """Turn humidifier off.""" return self.toggle_switch(False) def set_drying_mode_enabled(self, mode: bool) -> bool: """enable/disable drying filters after turning off.""" if mode not in (True, False): logger.debug( 'Invalid toggle passed to set_drying_mode_enabled - %s', mode ) return False head, body = self.build_api_dict('setDryingMode') body['payload']['data'] = { 'autoDryingSwitch': int(mode) } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True if isinstance(r, dict): logger.debug('Error in set_drying_mode_enabled response') else: logger.debug( 'Error in set_drying_mode_enabled api return json for %s', self.device_name ) return False def set_display_enabled(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'] = { 'screenSwitch': int(mode) } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug("Error toggling display - %s", self.device_name) return False def turn_on_display(self) -> bool: """Turn display on.""" return self.set_display_enabled(True) def turn_off_display(self): """Turn display off.""" return self.set_display_enabled(False) @property def display_state(self) -> bool: """Get display state.""" # This matches the values 0/1 set in set_display_enabled return self.details.get('display') == 1 def set_humidity(self, humidity: int) -> bool: """Set target humidity for humidity mode.""" 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'] = { 'targetHumidity': humidity } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity') return False def set_humidity_mode(self, mode: str) -> bool: """Set humidifier mode.""" if mode not in self.mist_modes: logger.debug('Invalid humidity mode used - %s', mode) logger.debug('Proper modes for this device are - %s', str(self.mist_modes)) return False head, body = self.build_api_dict('setHumidityMode') if not head and not body: return False body['payload']['data'] = { 'workMode': 'autoPro' if mode == 'auto' else mode } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity mode') return False def set_auto_mode(self) -> bool: """Set humidity mode to auto.""" return self.set_humidity_mode('auto') def set_manual_mode(self) -> bool: """Set humidity mode to manual.""" return self.set_humidity_mode('manual') def automatic_stop_on(self) -> bool: """Set humidity mode to auto.""" return self.set_humidity_mode('auto') def automatic_stop_off(self) -> bool: """Set humidity mode to manual.""" return self.set_humidity_mode('manual') def set_mist_level(self, level) -> bool: """Set humidifier mist level with int between 0 - 9.""" try: level = int(level) except ValueError: level = str(level) if level not in self.mist_levels: 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'] = { 'levelIdx': 0, 'virtualLevel': level, 'levelType': 'mist' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting mist level') return False @property def humidity_level(self): """Get Humidity level.""" return self.details['humidity'] # Duplicate for compatibility @property def humidity(self): """Get Humidity level.""" return self.details['humidity'] @property def mist_level(self): """Get current mist level.""" return self.details['mist_level'] @property def mist_virtual_level(self): """Get current mist virtual level.""" return self.details['mist_virtual_level'] @property def water_lacks(self): """If tank is empty return true.""" if 'water_lacks' in self.details: return bool(self.details['water_lacks']) return None @property def drying_mode_state(self): """True if humidifier is currently drying the filters, false otherwise.""" state = self.details.get('drying_mode', {}).get('dryingState') if state == 1: return 'on' if state == 2: return 'off' return None @property def drying_mode_seconds_remaining(self): """If drying_mode_state is on, how many seconds are remaining.""" return self.details.get('drying_mode', {}).get('dryingRemain') @property def drying_mode_enabled(self): """Checks if drying mode is enabled. Returns: bool: True if enabled, false if disabled """ enabled = self.details.get('drying_mode', {}).get('autoDryingSwitch') return None if enabled is None else bool(enabled) @property def drying_mode_level(self): """Drying mode level 1 = low, 2 = high.""" level = self.details.get('drying_mode', {}).get('dryingLevel') if level == 1: return 'low' if level == 2: return 'high' return None @property def temperature(self): """Current temperature.""" return self.details['temperature'] @property def auto_humidity(self): """Auto target humidity.""" return self.details['target_humidity'] @property def target_humidity(self): """The target humidity when in humidity mode.""" return self.details['target_humidity'] def display(self) -> None: """Return formatted device info to stdout.""" super().display() disp = [ ('Temperature', self.temperature, ''), ('Humidity: ', self.humidity_level, 'percent'), ('Target Humidity', self.target_humidity, 'percent'), ('Mode: ', self.mode, ''), ('Mist Virtual Level: ', self.details['mist_virtual_level'], ''), ('Mist Level: ', self.details['mist_level'], ''), ('Water Lacks: ', self.water_lacks, ''), ('Water Tank Lifted: ', bool(self.details['water_tank_lifted']), ''), ('Display On: ', bool(self.details['display']), ''), ('Filter Life', self.details['filter_life_percentage'], 'percent'), ('Drying Mode Enabled', self.drying_mode_enabled, ''), ('Drying Mode State', self.drying_mode_state, ''), ('Drying Mode Level', self.drying_mode_level, ''), ('Drying Mode Time Remaining', self.drying_mode_seconds_remaining, 'seconds'), ] for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') def displayJSON(self) -> str: """Return air purifier status and properties in JSON output.""" sup = super().displayJSON() sup_val = json.loads(sup) sup_val.update( { 'Temperature': self.temperature, 'Humidity': self.humidity_level, 'Target Humidity': self.target_humidity, 'Mode': self.mode, 'Mist Virtual Level': self.mist_virtual_level, 'Mist Level': self.mist_level, 'Water Lacks': self.details['water_lacks'], 'Water Tank Lifted': bool(self.details['water_tank_lifted']), 'Display On': bool(self.details['display']), 'Filter Life': self.details['filter_life_percentage'], 'Drying Mode Enabled': self.drying_mode_enabled, 'Drying Mode State': self.drying_mode_state, 'Drying Mode Level': self.drying_mode_level, 'Drying Mode Time Remaining': self.drying_mode_seconds_remaining, } ) return json.dumps(sup_val, indent=4) class VeSyncHumid1000S(VeSyncHumid200300S): """Levoit OasisMist 1000S Specific class.""" def __init__(self, details, manager): """Initialize levoit 1000S device class.""" super().__init__(details, manager) self.connection_status = details.get('deviceProp', {}).get( 'connectionStatus', None) self._api_modes = ['getHumidifierStatus', 'setAutoStopSwitch', 'setSwitch', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setDisplay'] def build_humid_dict(self, dev_dict: Dict[str, str]) -> None: """Build humidifier status dictionary.""" super().build_humid_dict(dev_dict) self.device_status = 'off' if dev_dict.get('powerSwitch', 0) == 0 else 'on' self.details['mist_virtual_level'] = dev_dict.get( 'virtualLevel', 0) self.details['mist_level'] = dev_dict.get('mistLevel', 0) self.details['mode'] = dev_dict.get('workMode', 'manual') self.details['water_lacks'] = bool(dev_dict.get('waterLacksState', 0)) self.details['humidity_high'] = bool(int(dev_dict.get('targetHumidity', 0)) < int(dev_dict.get('humidity', 0))) self.details['water_tank_lifted'] = bool(dev_dict.get( 'waterTankLifted', 0)) self.details['automatic_stop_reach_target'] = bool(dev_dict.get( 'autoStopState', 1 )) self.details['display'] = bool(dev_dict['screenState']) def build_config_dict(self, conf_dict): """Build configuration dict for humidifier.""" self.config['auto_target_humidity'] = conf_dict.get( 'targetHumidity', 0) self.config['display'] = bool(conf_dict.get('screenSwitch', 0)) self.config['automatic_stop'] = bool(conf_dict.get('autoStopSwitch', 1)) def get_details(self) -> None: """Build 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_object=body, ) if r is None or not isinstance(r, dict): logger.debug("Error getting status of %s ", self.device_name) return 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.connection_status = 'online' self.build_humid_dict(inner_result) self.build_config_dict(inner_result) else: logger.debug('error in inner result dict from humidifier') elif r.get('code') == -11300030: logger.debug('%s device offline', self.device_name) self.connection_status = 'offline' self.device_status = 'off' else: logger.debug('Error in humidifier response') 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'] = { 'screenSwitch': int(mode) } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug("Error toggling purifier display - %s", self.device_name) return False def set_humidity_mode(self, mode: str) -> bool: """Set humidifier mode - sleep, auto or manual.""" if mode.lower() not in self.mist_modes: logger.debug('Invalid humidity mode used - %s', mode) logger.debug('Proper modes for this device are - %s', str(self.mist_modes)) return False head, body = self.build_api_dict('setHumidityMode') if not head and not body: return False body['payload']['data'] = { 'workMode': mode.lower() } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity mode') return False def set_sleep_mode(self): """Set humifier to manual mode with 1 mist level.""" return self.set_humidity_mode('sleep') def set_mist_level(self, level) -> bool: """Set humidifier mist level with int.""" try: level = int(level) except ValueError: level = str(level) if level not in self.mist_levels: logger.debug('Humidifier mist level out of range') return False head, body = self.build_api_dict('setVirtualLevel') if not head and not body: return False body['payload']['data'] = { 'levelIdx': 0, 'virtualLevel': level, 'levelType': 'mist' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting mist level') return False 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': { 'powerSwitch': int(toggle), 'switchIdx': 0 }, 'method': 'setSwitch', 'source': 'APP' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): if toggle: self.device_status = 'on' else: self.device_status = 'off' return True logger.debug("Error toggling humidifier - %s", self.device_name) return False def set_humidity(self, humidity: int) -> bool: """Set target 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'] = { 'targetHumidity': humidity } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and Helpers.code_check(r): return True logger.debug('Error setting humidity') return False def set_automatic_stop(self, mode: bool) -> bool: """Set 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('setAutoStopSwitch') if not head and not body: return False body['payload']['data'] = { 'autoStopSwitch': int(mode) } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', method='post', headers=head, json_object=body, ) if r is not None and 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 pyvesync-2.1.18/src/pyvesync/vesynckitchen.py000066400000000000000000000561571475441503300214050ustar00rootroot00000000000000"""VeSync Kitchen Devices.""" import json import logging import time from functools import wraps from typing import Optional, Union, Set from dataclasses import dataclass from pyvesync.vesyncbasedevice import VeSyncBaseDevice from pyvesync.helpers import Helpers as helpers logger = logging.getLogger(__name__) kitchen_features: dict = { 'Cosori3758L': { 'module': 'VeSyncAirFryer158', 'models': ['CS137-AF/CS158-AF', 'CS158-AF', 'CS137-AF', 'CS358-AF'], 'features': [], } } def model_dict() -> dict: """Build purifier and humidifier model dictionary.""" model_modules = {} for dev_dict in kitchen_features.values(): for model in dev_dict['models']: model_modules[model] = dev_dict['module'] return model_modules def model_features(dev_type: str) -> dict: """Get features from device type.""" for dev_dict in kitchen_features.values(): if dev_type in dev_dict['models']: return dev_dict raise ValueError('Device not configured') kitchen_classes: Set[str] = {v['module'] for k, v in kitchen_features.items()} kitchen_modules: dict = model_dict() __all__ = list(kitchen_classes) # Status refresh interval in seconds # API calls outside of interval are automatically refreshed # Set VeSyncAirFryer158.refresh_interval to 0 to refresh every call # Set to None or -1 to disable auto-refresh REFRESH_INTERVAL = 60 RECIPE_ID = 1 RECIPE_TYPE = 3 CUSTOM_RECIPE = 'Manual Cook' COOK_MODE = 'custom' def check_status(func): """Check interval between updates.""" @wraps(func) def wrapper(self, *args, **kwargs): seconds_elapsed = int(time.time()) - self.last_update logger.debug("Seconds elapsed between updates: %s", seconds_elapsed) refresh = False if self.refresh_interval is None: refresh = bool(seconds_elapsed > REFRESH_INTERVAL) elif self.refresh_interval == 0: refresh = True elif self.refresh_interval > 0: refresh = bool(seconds_elapsed > self.refresh_interval) if refresh is True: logger.debug("Updating status, %s seconds elapsed", seconds_elapsed) self.update() return func(self, *args, **kwargs) return wrapper @dataclass(init=False, eq=False, repr=False) class FryerStatus: """Dataclass for air fryer status.""" ready_start: bool = False preheat: bool = False cook_status: Optional[str] = None current_temp: Optional[int] = None cook_set_temp: Optional[int] = None cook_set_time: Optional[int] = None cook_last_time: Optional[int] = None last_timestamp: Optional[int] = None preheat_set_time: Optional[int] = None preheat_last_time: Optional[int] = None _temp_unit: Optional[str] = None @property def is_resumable(self) -> bool: """Return if cook is resumable.""" if self.cook_status in ['cookStop', 'preheatStop']: if self.cook_set_time is not None: return self.cook_set_time > 0 if self.preheat_set_time is not None: return self.preheat_set_time > 0 return False @property def temp_unit(self) -> Optional[str]: """Return temperature unit.""" return self._temp_unit @temp_unit.setter def temp_unit(self, temp_unit: str): """Set temperature unit.""" if temp_unit.lower() in ['f', 'fahrenheit']: self._temp_unit = 'fahrenheit' elif temp_unit.lower() in ['c', 'celsius']: self._temp_unit = 'celsius' else: raise ValueError(f'Invalid temperature unit - {temp_unit}') @property def preheat_time_remaining(self) -> int: """Return preheat time remaining.""" if self.preheat is False or self.cook_status == 'preheatEnd': return 0 if self.cook_status in ['pullOut', 'preheatStop']: if self.preheat_last_time is None: return 0 return int(self.preheat_last_time // 60) if self.preheat_last_time is not None and self.last_timestamp is not None: return int(max((self.preheat_last_time - (int(time.time()) - self.last_timestamp)) // 60, 0)) return 0 @property def cook_time_remaining(self) -> int: """Returns the amount of time remaining if cooking.""" if self.preheat is True or self.cook_status == 'cookEnd': return 0 if self.cook_status in ['pullOut', 'cookStop']: if self.cook_last_time is None: return 0 return int(max(self.cook_last_time // 60, 0)) if self.cook_last_time is not None and self.last_timestamp is not None: return int(max((self.cook_last_time - (int(time.time()) - self.last_timestamp)) // 60, 0)) return 0 @property def remaining_time(self): """Return minutes remaining if cooking/heating.""" if self.preheat is True: return self.preheat_time_remaining return self.cook_time_remaining @property def is_running(self) -> bool: """Return if cooking or heating.""" return bool(self.cook_status in ['cooking', 'heating']) and \ bool(self.remaining_time > 0) @property def is_cooking(self) -> bool: """Return if cooking.""" return self.cook_status == 'cooking' and self.remaining_time > 0 @property def is_heating(self) -> bool: """Return if heating.""" return self.cook_status == 'heating' and self.remaining_time > 0 def status_request(self, json_cmd: dict) -> None: # pylint: disable=R1260 """Set status from jsonCmd of API call.""" self.last_timestamp = None if not isinstance(json_cmd, dict): return self.preheat = False preheat = json_cmd.get('preheat') cook = json_cmd.get('cookMode') if isinstance(preheat, dict): self.preheat = True if preheat.get('preheatStatus') == 'stop': self.cook_status = 'preheatStop' elif preheat.get('preheatStatus') == 'heating': self.cook_status = 'heating' self.last_timestamp = int(time.time()) self.preheat_set_time = preheat.get('preheatSetTime', self.preheat_set_time) if preheat.get('preheatSetTime') is not None: self.preheat_last_time = preheat.get('preheatSetTime') self.cook_set_temp = preheat.get('targetTemp', self.cook_set_temp) self.cook_set_time = preheat.get('cookSetTime', self.cook_set_time) self.cook_last_time = None elif preheat.get('preheatStatus') == 'end': self.cook_status = 'preheatEnd' self.preheat_last_time = 0 elif isinstance(cook, dict): self.clear_preheat() if cook.get('cookStatus') == 'stop': self.cook_status = 'cookStop' elif cook.get('cookStatus') == 'cooking': self.cook_status = 'cooking' self.last_timestamp = int(time.time()) self.cook_set_time = cook.get('cookSetTime', self.cook_set_time) self.cook_set_temp = cook.get('cookSetTemp', self.cook_set_temp) self.current_temp = cook.get('currentTemp', self.current_temp) self.temp_unit = cook.get('tempUnit', self.temp_unit) elif cook.get('cookStatus') == 'end': self.set_standby() self.cook_status = 'cookEnd' def clear_preheat(self): """Clear preheat status.""" self.preheat = False self.preheat_set_time = None self.preheat_last_time = None def set_standby(self): """Clear cooking status.""" self.cook_status = 'standby' self.clear_preheat() self.cook_last_time = None self.current_temp = None self.cook_set_time = None self.cook_set_temp = None self.last_timestamp = None def status_response(self, return_status: dict) -> None: """Set status of Air Fryer Based on API Response.""" self.last_timestamp = None self.preheat = False self.cook_status = return_status.get('cookStatus') if self.cook_status == 'standby': self.set_standby() return # If drawer is pulled out, set standby if resp does not contain other details if self.cook_status == 'pullOut': self.last_timestamp = None if 'currentTemp' not in return_status or 'tempUnit' not in return_status: self.set_standby() self.cook_status = 'pullOut' return if return_status.get('preheatLastTime') is not None or \ self.cook_status in ['heating', 'preheatStop', 'preheatEnd']: self.preheat = True self.cook_set_time = return_status.get('cookSetTime', self.cook_set_time) self.cook_last_time = return_status.get('cookLastTime') self.current_temp = return_status.get('curentTemp') self.cook_set_temp = return_status.get('targetTemp', return_status.get('cookSetTemp')) self.temp_unit = return_status.get('tempUnit', self.temp_unit) # Always keep set self.preheat_set_time = return_status.get('preheatSetTime') self.preheat_last_time = return_status.get('preheatLastTime') # Set last_time timestamp if cooking if self.cook_status in ['cooking', 'heating']: self.last_timestamp = int(time.time()) if self.cook_status == 'preheatEnd': self.preheat_last_time = 0 self.cook_last_time = None if self.cook_status == 'cookEnd': self.cook_last_time = 0 # If Cooking, clear preheat status if self.cook_status in ['cooking', 'cookStop', 'cookEnd']: self.clear_preheat() class VeSyncAirFryer158(VeSyncBaseDevice): """Cosori Air Fryer Class.""" def __init__(self, details, manager): """Init the VeSync Air Fryer 158 class.""" super().__init__(details, manager) self.fryer_status = FryerStatus() if self.pid is None: self.get_pid() self.fryer_status.temp_unit = self.get_temp_unit() self.ready_start = self.get_remote_cook_mode() self.last_update = int(time.time()) self.refresh_interval = 0 def get_body(self, method: Optional[str] = None) -> dict: """Return body of api calls.""" body = { **helpers.req_body(self.manager, 'bypass'), 'cid': self.cid, 'userCountryCode': self.manager.country_code, 'debugMode': False } if method is not None: body['method'] = method return body def get_status_body(self, cmd_dict: dict) -> dict: """Return body of api calls.""" body = self.get_body() body.update({ 'uuid': self.uuid, 'configModule': self.config_module, 'jsonCmd': cmd_dict, 'pid': self.pid, 'accountID': self.manager.account_id } ) return body def get_temp_unit(self): """Get Air Fryer Configuration.""" body = self.get_body('configurationsV2') r, _ = helpers.call_api('/cloud/v2/deviceManaged/configurationsV2', 'post', json_object=body) if not isinstance(r, dict) or r.get('code') != 0 \ or not isinstance(r.get('result'), dict): logger.debug('Failed to get config for %s', self.device_name) return result = r.get('result') if result is not None: return result.get('airFryerInfo', {}).get('workTempUnit', 'f') def get_remote_cook_mode(self): """Get the cook mode.""" body = self.get_body('getRemoteCookMode158') r, _ = helpers.call_api('/cloud/v1/deviceManaged/getRemoteCookMode158', 'post', json_object=body) if not isinstance(r, dict) or r.get('code') != 0 \ or not isinstance(r.get('result'), dict): return False return r.get('result', {}).get('readyStart', False) @property def temp_unit(self) -> Optional[str]: """Return temp unit.""" return self.fryer_status.temp_unit @property def current_temp(self) -> Optional[int]: """Return current temperature.""" return self.fryer_status.current_temp @property def cook_set_temp(self) -> Optional[int]: """Return set temperature.""" return self.fryer_status.cook_set_temp @property def preheat(self) -> bool: """Return preheat status.""" return self.fryer_status.preheat @property def cook_last_time(self) -> Optional[int]: """Return cook last time.""" return self.fryer_status.cook_last_time @property def cook_set_time(self) -> Optional[int]: """Return cook set time.""" return self.fryer_status.cook_set_time @property def preheat_last_time(self) -> Optional[int]: """Return preheat last time.""" if self.preheat is True: return self.fryer_status.preheat_last_time return None @property def preheat_set_time(self) -> Optional[int]: """Return preheat set time.""" if self.preheat is True: return self.fryer_status.preheat_set_time return None @property def cook_status(self) -> Optional[str]: """Return the cook status.""" return self.fryer_status.cook_status @property def is_cooking(self) -> bool: """Return True if air fryer is heating.""" return self.fryer_status.is_cooking @property def is_heating(self) -> bool: """Return True if air fryer is preheating.""" return self.fryer_status.is_heating @property def is_running(self): """Return True if cooking or preheating finished.""" return self.fryer_status.is_running @property def remaining_time(self) -> Optional[int]: """Return time remaining in minutes or None if not cooking/heating.""" return self.fryer_status.remaining_time def get_details(self): """Get Air Fryer Status and Details.""" cmd = {'getStatus': 'status'} req_body = self.get_status_body(cmd) url = '/cloud/v1/deviceManaged/bypass' resp, _ = helpers.call_api(url, 'post', json_object=req_body) if resp is None: logger.debug('Failed to get details for %s', self.device_name) return False if resp.get('code') == -11300030: logger.debug('%s is offline', self.device_name) self.fryer_status.set_standby() self.fryer_status.cook_status = 'offline' return False if resp.get('code') != 0: logger.debug('Failed to get details for %s \n with code: %s and message: %s', self.device_name, str(resp.get("code", 0)), resp.get("msg", '')) return False return_status = resp.get('result', {}).get('returnStatus') if return_status is None: return False self.fryer_status.status_response(return_status) return True @check_status def end(self): """End the cooking process.""" if self.preheat is False \ and self.fryer_status.cook_status in ['cookStop', 'cooking']: cmd = { 'cookMode': { 'cookStatus': 'end' } } elif self.preheat is True \ and self.fryer_status.cook_status in ['preheatStop', 'heating']: cmd = { 'preheat': { 'cookStatus': 'end' } } else: logger.debug('Cannot end %s as it is not cooking or preheating', self.device_name) return False if self._status_api(cmd) is True: self.fryer_status.set_standby() return True return False @check_status def pause(self) -> bool: """Pause the cooking process.""" if self.cook_status not in ['cooking', 'heating']: logger.debug('Cannot pause %s as it is not cooking or preheating', self.device_name) return False if self.preheat is True: cmd = { 'preheat': { 'preheatStatus': 'stop' } } else: cmd = { 'cookMode': { 'cookStatus': 'stop' } } if self._status_api(cmd) is True: if self.preheat is True: self.fryer_status.cook_status = 'preheatStop' else: self.fryer_status.cook_status = 'cookStop' return True return False def _validate_temp(self, set_temp: int) -> bool: """Temperature validation.""" if self.fryer_status.temp_unit == 'fahrenheight': if set_temp < 200 or set_temp > 400: logger.debug('Invalid temperature %s for %s', set_temp, self.device_name) return False if self.fryer_status.temp_unit == 'celsius': if set_temp < 75 or set_temp > 205: logger.debug('Invalid temperature %s for %s', set_temp, self.device_name) return False return True @check_status def cook(self, set_temp: int, set_time: int) -> bool: """Set cook time and temperature in Minutes.""" if self._validate_temp(set_temp) is False: return False return self._set_cook(set_temp, set_time) @check_status def resume(self) -> bool: """Resume paused preheat or cook.""" if self.cook_status not in ['preheatStop', 'cookStop']: logger.debug('Cannot resume %s as it is not paused', self.device_name) return False if self.preheat is True: cmd = { 'preheat': { 'preheatStatus': 'heating' } } else: cmd = { 'cookMode': { 'cookStatus': 'cooking' } } if self._status_api(cmd) is True: if self.preheat is True: self.fryer_status.cook_status = 'heating' else: self.fryer_status.cook_status = 'cooking' return True return False @check_status def set_preheat(self, target_temp: int, cook_time: int) -> bool: """Set preheat mode with cooking time.""" if self.cook_status not in ['standby', 'cookEnd', 'preheatEnd']: logger.debug('Cannot set preheat for %s as it is not in standby', self.device_name) return False if self._validate_temp(target_temp) is False: return False cmd = self._cmd_api_dict cmd['preheatSetTime'] = 5 cmd['preheatStatus'] = 'heating' cmd['targetTemp'] = target_temp cmd['cookSetTime'] = cook_time json_cmd = { 'preheat': cmd } return self._status_api(json_cmd) # @check_status def cook_from_preheat(self) -> bool: """Start Cook when preheat has ended.""" if self.preheat is False or self.cook_status != 'preheatEnd': logger.debug('Cannot start cook from preheat for %s', self.device_name) return False return self._set_cook(status='cooking') def update(self): """Update the device details.""" self.get_details() @staticmethod def fryer_code_check(code: Union[str, int]) -> Optional[str]: """Return the code description.""" if isinstance(code, str): try: code = int(code) except ValueError: return None if code == 11903000: return 'Error pausing, air fryer is not cooking.' if code == 11902000: return 'Error setting cook mode, air fryer is already cooking' if str(abs(code))[0:5] == 11300: return 'Air fryer is offline' return None @property def _cmd_api_base(self) -> dict: """Return Base api dictionary for setting status.""" return { "mode": COOK_MODE, "accountId": self.manager.account_id, } @property def _cmd_api_dict(self) -> dict: """Return API dictionary for setting status.""" cmd = self._cmd_api_base cmd.update({ "appointmentTs": 0, "recipeId": RECIPE_ID, "readyStart": self.ready_start, "recipeType": RECIPE_TYPE, "customRecipe": CUSTOM_RECIPE, }) return cmd def _set_cook(self, set_temp: Optional[int] = None, set_time: Optional[int] = None, status: str = 'cooking') -> bool: if set_temp is not None and set_time is not None: set_cmd = self._cmd_api_dict set_cmd['cookSetTime'] = set_time set_cmd['cookSetTemp'] = set_temp else: set_cmd = self._cmd_api_base set_cmd['cookStatus'] = status cmd = {'cookMode': set_cmd} return self._status_api(cmd) def _status_api(self, json_cmd: dict): """Set API status with jsonCmd.""" body = self.get_status_body(json_cmd) url = '/cloud/v1/deviceManaged/bypass' resp, _ = helpers.call_api(url, 'post', json_object=body) if resp is None: logger.debug('Failed to set status for %s - No response from API', self.device_name) return False if resp.get('code') != 0 and resp.get('code') is not None: debug_msg = self.fryer_code_check(resp.get('code')) logger.debug('Failed to set status for %s \n Code: %s and message: %s \n' ' %s', self.device_name, resp.get("code"), resp.get("msg"), debug_msg) return False self.last_update = int(time.time()) self.fryer_status.status_request(json_cmd) self.update() return True def displayJSON(self) -> str: """Display JSON of device details.""" sup = super().displayJSON() sup_dict = json.loads(sup) sup_dict['cook_status'] = self.cook_status sup_dict['temp_unit'] = self.temp_unit if self.cook_status not in ['standby', 'cookEnd', 'preheatEnd']: status_dict = { 'preheat': self.preheat, 'current_temp': self.current_temp, 'cook_set_temp': self.cook_set_temp, 'cook_set_time': self.cook_set_time, 'cook_last_time': self.cook_last_time } if self.preheat is True: preheat_dict = { 'preheat_last_time': self.preheat_last_time, 'preheat_set_time': self.preheat_set_time } status_dict.update(preheat_dict) sup_dict.update(status_dict) return json.dumps(sup_dict, indent=4) pyvesync-2.1.18/src/pyvesync/vesyncoutlet.py000066400000000000000000000650011475441503300212600ustar00rootroot00000000000000"""Etekcity Outlets.""" import logging import time import json from abc import ABCMeta, abstractmethod from pyvesync.helpers import Helpers from pyvesync.vesyncbasedevice import VeSyncBaseDevice logger = logging.getLogger(__name__) outlet_config = { 'wifi-switch-1.3': { 'module': 'VeSyncOutlet7A'}, 'ESW03-USA': { 'module': 'VeSyncOutlet10A'}, 'ESW01-EU': { 'module': 'VeSyncOutlet10A'}, 'ESW15-USA': { 'module': 'VeSyncOutlet15A'}, 'ESO15-TB': { 'module': 'VeSyncOutdoorPlug'}, 'BSDOG01': { 'module': 'VeSyncOutletBSDGO1'}, } outlet_modules = {k: v['module'] for k, v in outlet_config.items()} __all__ = list(outlet_modules.values()) + ['outlet_modules'] class VeSyncOutlet(VeSyncBaseDevice): """Base class for Etekcity Outlets.""" __metaclass__ = ABCMeta def __init__(self, details, manager): """Initilize VeSync Outlet base class.""" super().__init__(details, manager) self.details = {} self.energy = {} self.update_energy_ts = None self._energy_update_interval = manager.energy_update_interval @property def update_time_check(self) -> bool: """Test if energy update interval has been exceeded.""" if self.update_energy_ts is None: return True if ((time.time() - self.update_energy_ts) > self._energy_update_interval): return True return False @abstractmethod def turn_on(self) -> bool: """Return True if device has beeeen turned on.""" @abstractmethod def turn_off(self) -> bool: """Return True if device has beeeen turned off.""" @abstractmethod def get_details(self) -> None: """Build details dictionary.""" @abstractmethod def get_weekly_energy(self) -> None: """Build weekly energy history dictionary.""" @abstractmethod def get_monthly_energy(self) -> None: """Build Monthly Energy History Dictionary.""" @abstractmethod def get_yearly_energy(self): """Build Yearly Energy Dictionary.""" @abstractmethod def get_config(self): """Get configuration and firmware details.""" def update(self): """Get Device Energy and Status.""" self.get_details() def update_energy(self, bypass_check: bool = False): """Build weekly, monthly and yearly dictionaries.""" if bypass_check or (not bypass_check and self.update_time_check): self.update_energy_ts = time.time() self.get_weekly_energy() if 'week' in self.energy: self.get_monthly_energy() self.get_yearly_energy() if not bypass_check: self.update_energy_ts = time.time() @property def active_time(self) -> int: """Return active time of a device in minutes.""" return self.details.get('active_time', 0) @property def energy_today(self) -> float: """Return energy.""" return self.details.get('energy', 0) @property def power(self) -> float: """Return current power in watts.""" return float(self.details.get('power', 0)) @property def voltage(self) -> float: """Return current voltage.""" return float(self.details.get('voltage', 0)) @property def monthly_energy_total(self) -> float: """Return total energy usage over the month.""" return self.energy.get('month', {}).get('total_energy', 0) @property def weekly_energy_total(self) -> float: """Return total energy usage over the week.""" return self.energy.get('week', {}).get('total_energy', 0) @property def yearly_energy_total(self) -> float: """Return total energy usage over the year.""" return self.energy.get('year', {}).get('total_energy', 0) def display(self): """Return formatted device info to stdout.""" super().display() disp = [ ('Active Time : ', self.active_time, ' minutes'), ('Energy: ', self.energy_today, ' kWh'), ('Power: ', self.power, ' Watts'), ('Voltage: ', self.voltage, ' Volts'), ('Energy Week: ', self.weekly_energy_total, ' kWh'), ('Energy Month: ', self.monthly_energy_total, ' kWh'), ('Energy Year: ', self.yearly_energy_total, ' kWh'), ] for line in disp: print(f'{line[0]:.<30} {line[1]} {line[2]}') def displayJSON(self): """Return JSON details for outlet.""" sup = super().displayJSON() sup_val = json.loads(sup) sup_val.update( { 'Active Time': str(self.active_time), 'Energy': str(self.energy_today), 'Power': str(self.power), 'Voltage': str(self.voltage), 'Energy Week': str(self.weekly_energy_total), 'Energy Month': str(self.monthly_energy_total), 'Energy Year': str(self.yearly_energy_total), } ) return json.dumps(sup_val, indent=4) class VeSyncOutlet7A(VeSyncOutlet): """Etekcity 7A Round Outlet Class.""" def __init__(self, details, manager): """Initilize Etekcity 7A round outlet class.""" super().__init__(details, manager) self.det_keys = ['deviceStatus', 'activeTime', 'energy', 'power', 'voltage'] self.energy_keys = ['energyConsumptionOfToday', 'maxEnergy', 'totalEnergy'] def get_details(self) -> None: """Get 7A outlet details.""" r, _ = Helpers.call_api( '/v1/device/' + self.cid + '/detail', 'get', headers=Helpers.req_headers(self.manager), ) if r is not None and all(x in r for x in self.det_keys): self.device_status = r.get('deviceStatus', self.device_status) self.details['active_time'] = r.get('activeTime', 0) self.details['energy'] = r.get('energy', 0) power = r.get('power', '0') self.details['power'] = self.parse_energy_detail(power) voltage = r.get('voltage', 0) self.details['voltage'] = self.parse_energy_detail(voltage) else: logger.debug('Unable to get %s details', self.device_name) @staticmethod def parse_energy_detail(energy): """Parse energy details to be compatible with new and old firmware.""" try: if isinstance(energy, str) and ':' in energy: power = round(float(Helpers.calculate_hex(energy)), 2) else: power = float(energy) except ValueError: logger.debug('Error parsing power response - %s', energy) power = 0 return power def get_weekly_energy(self) -> None: """Get 7A outlet weekly energy info and buld weekly energy dict.""" r, _ = Helpers.call_api( '/v1/device/' + self.cid + '/energy/week', 'get', headers=Helpers.req_headers(self.manager), ) if r is not None and all(x in r for x in self.energy_keys): self.energy['week'] = Helpers.build_energy_dict(r) else: logger.debug('Unable to get %s weekly data', self.device_name) def get_monthly_energy(self) -> None: """Get 7A outlet monthly energy info and buld monthly energy dict.""" r, _ = Helpers.call_api( '/v1/device/' + self.cid + '/energy/month', 'get', headers=Helpers.req_headers(self.manager), ) if r is not None and all(x in r for x in self.energy_keys): self.energy['month'] = Helpers.build_energy_dict(r) else: logger.warning('Unable to get %s monthly data', self.device_name) def get_yearly_energy(self) -> None: """Get 7A outlet yearly energy info and build yearly energy dict.""" r, _ = Helpers.call_api( '/v1/device/' + self.cid + '/energy/year', 'get', headers=Helpers.req_headers(self.manager), ) if r is not None and all(x in r for x in self.energy_keys): self.energy['year'] = Helpers.build_energy_dict(r) else: logger.debug('Unable to get %s yearly data', self.device_name) def turn_on(self) -> bool: """Turn 7A outlet on - return True if successful.""" _, status_code = Helpers.call_api( '/v1/wifi-switch-1.3/' + self.cid + '/status/on', 'put', headers=Helpers.req_headers(self.manager), ) if status_code is not None and status_code == 200: self.device_status = 'on' return True logger.warning('Error turning %s on', self.device_name) return False def turn_off(self) -> bool: """Turn 7A outlet off - return True if successful.""" _, status_code = Helpers.call_api( '/v1/wifi-switch-1.3/' + self.cid + '/status/off', 'put', headers=Helpers.req_headers(self.manager), ) if status_code is not None and status_code == 200: self.device_status = 'off' return True logger.warning('Error turning %s off', self.device_name) return False def get_config(self) -> None: """Get 7A outlet configuration info.""" r, _ = Helpers.call_api( '/v1/device/' + self.cid + '/configurations', 'get', headers=Helpers.req_headers(self.manager), ) if 'currentFirmVersion' in r: self.config = Helpers.build_config_dict(r) else: logger.debug('Error getting configuration info for %s', self.device_name) class VeSyncOutlet10A(VeSyncOutlet): """Etekcity 10A Round Outlets.""" def __init__(self, details, manager): """Initialize 10A outlet class.""" super().__init__(details, manager) def get_details(self) -> None: """Get 10A outlet details.""" body = Helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid r, _ = Helpers.call_api( '/10a/v1/device/devicedetail', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(r): self.device_status = r.get('deviceStatus', self.device_status) self.connection_status = r.get('connectionStatus', self.connection_status) self.details = Helpers.build_details_dict(r) else: logger.debug('Unable to get %s details', self.device_name) def get_config(self) -> None: """Get 10A outlet configuration info.""" body = Helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = Helpers.call_api( '/10a/v1/device/configurations', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(r): self.config = Helpers.build_config_dict(r) else: logger.debug('Error getting %s config info', self.device_name) def get_weekly_energy(self) -> None: """Get 10A outlet weekly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_week') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/10a/v1/device/energyweek', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['week'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s weekly data', self.device_name) def get_monthly_energy(self) -> None: """Get 10A outlet monthly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_month') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/10a/v1/device/energymonth', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['month'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s monthly data', self.device_name) def get_yearly_energy(self) -> None: """Get 10A outlet yearly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_year') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/10a/v1/device/energyyear', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['year'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s yearly data', self.device_name) def turn_on(self) -> bool: """Turn 10A outlet on - return True if successful.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'on' response, _ = Helpers.call_api( '/10a/v1/device/devicestatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.device_status = 'on' return True logger.warning('Error turning %s on', self.device_name) return False def turn_off(self) -> bool: """Turn 10A outlet off - return True if successful.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'off' response, _ = Helpers.call_api( '/10a/v1/device/devicestatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.device_status = 'off' return True logger.warning('Error turning %s off', self.device_name) return False class VeSyncOutlet15A(VeSyncOutlet): """Class for Etekcity 15A Rectangular Outlets.""" def __init__(self, details, manager): """Initialize 15A rectangular outlets.""" super().__init__(details, manager) self.nightlight_status = 'off' self.nightlight_brightness = 0 def get_details(self) -> None: """Get 15A outlet details.""" body = Helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid r, _ = Helpers.call_api( '/15a/v1/device/devicedetail', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) attr_list = ( 'deviceStatus', 'activeTime', 'energy', 'power', 'voltage', 'nightLightStatus', 'nightLightAutomode', 'nightLightBrightness', ) if Helpers.code_check(r) and all(k in r for k in attr_list): self.device_status = r.get('deviceStatus') self.connection_status = r.get('connectionStatus') self.nightlight_status = r.get('nightLightStatus') self.nightlight_brightness = r.get('nightLightBrightness') self.details = Helpers.build_details_dict(r) else: logger.debug('Unable to get %s details', self.device_name) def get_config(self) -> None: """Get 15A outlet configuration info.""" body = Helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = Helpers.call_api( '/15a/v1/device/configurations', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(r): self.config = Helpers.build_config_dict(r) else: logger.debug('Unable to get %s config info', self.device_name) def get_weekly_energy(self) -> None: """Get 15A outlet weekly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_week') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/15a/v1/device/energyweek', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['week'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s weekly data', self.device_name) def get_monthly_energy(self) -> None: """Get 15A outlet monthly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_month') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/15a/v1/device/energymonth', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['month'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s monthly data', self.device_name) def get_yearly_energy(self) -> None: """Get 15A outlet yearly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_year') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/15a/v1/device/energyyear', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['year'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s yearly data', self.device_name) def turn_on(self) -> bool: """Turn 15A outlet on - return True if successful.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'on' response, _ = Helpers.call_api( '/15a/v1/device/devicestatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.device_status = 'on' return True logger.warning('Error turning %s on', self.device_name) return False def turn_off(self) -> bool: """Turn 15A outlet off - return True if successful.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = 'off' response, _ = Helpers.call_api( '/15a/v1/device/devicestatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.device_status = 'off' return True logger.warning('Error turning %s off', self.device_name) return False def turn_on_nightlight(self) -> bool: """Turn on nightlight.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['mode'] = 'auto' response, _ = Helpers.call_api( '/15a/v1/device/nightlightstatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): return True logger.debug('Error turning on %s nightlight', self.device_name) return False def turn_off_nightlight(self) -> bool: """Turn Off Nightlight.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['mode'] = 'manual' response, _ = Helpers.call_api( '/15a/v1/device/nightlightstatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): return True logger.debug('Error turning off %s nightlight', self.device_name) return False class VeSyncOutdoorPlug(VeSyncOutlet): """Class to hold Etekcity outdoor outlets.""" def __init__(self, details, manager): """Initialize Etekcity Outdoor Plug class.""" super().__init__(details, manager) def get_details(self) -> None: """Get details for outdoor outlet.""" body = Helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid r, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/devicedetail', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(r): self.details = Helpers.build_details_dict(r) self.connection_status = r.get('connectionStatus') dev_no = self.sub_device_no sub_device_list = r.get('subDevices') if sub_device_list and dev_no <= len(sub_device_list): self.device_status = sub_device_list[(dev_no + -1)].get( 'subDeviceStatus' ) return logger.debug('Unable to get %s details', self.device_name) def get_config(self) -> None: """Get configuration info for outdoor outlet.""" body = Helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/configurations', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(r): self.config = Helpers.build_config_dict(r) logger.debug('Error getting %s config info', self.device_name) def get_weekly_energy(self) -> None: """Get outdoor outlet weekly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_week') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/energyweek', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['week'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s weekly data', self.device_name) def get_monthly_energy(self) -> None: """Get outdoor outlet monthly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_month') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/energymonth', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['month'] = Helpers.build_energy_dict(response) logger.debug('Unable to get %s monthly data', self.device_name) def get_yearly_energy(self) -> None: """Get outdoor outlet yearly energy info and populate energy dict.""" body = Helpers.req_body(self.manager, 'energy_year') body['uuid'] = self.uuid response, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/energyyear', 'post', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.energy['year'] = Helpers.build_energy_dict(response) else: logger.debug('Unable to get %s yearly data', self.device_name) def toggle(self, status) -> bool: """Toggle power for outdoor outlet.""" body = Helpers.req_body(self.manager, 'devicestatus') body['uuid'] = self.uuid body['status'] = status body['switchNo'] = self.sub_device_no response, _ = Helpers.call_api( '/outdoorsocket15a/v1/device/devicestatus', 'put', headers=Helpers.req_headers(self.manager), json_object=body, ) if Helpers.code_check(response): self.device_status = status return True logger.warning('Error turning %s %s', self.device_name, status) return False def turn_on(self) -> bool: """Turn on outlet.""" return bool(self.toggle('on')) def turn_off(self) -> bool: """Turn off outlet.""" return bool(self.toggle('off')) class VeSyncOutletBSDGO1(VeSyncOutlet): """VeSync BSDGO1 smart plug.""" def __init__(self, details, manager): """Initialize BSDGO1 smart plug class.""" super().__init__(details, manager) def get_details(self) -> None: """Get BSDGO1 device details.""" body = Helpers.req_body(self.manager, 'bypassV2') body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'method': 'getProperty', 'source': 'APP', 'data': {} } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=Helpers.req_header_bypass(), json_object=body, ) if Helpers.code_check(r): self.device_status = 'on' if r.get('result', {}).get( 'powerSwitch_1') == 1 else 'off' else: logger.debug('Error getting %s details', self.device_name) def turn_on(self) -> bool: """Turn BSDGO1 outlet on.""" return self._set_power(True) def turn_off(self) -> bool: """Turn BSDGO1 outlet off.""" return self._set_power(False) def _set_power(self, power: bool) -> bool: """Set power state of BSDGO1 outlet.""" body = Helpers.req_body(self.manager, 'bypassV2') body['cid'] = self.cid body['configModule'] = self.config_module body['payload'] = { 'data': {'powerSwitch_1': 1 if power else 0}, 'method': 'setProperty', 'source': 'APP' } r, _ = Helpers.call_api( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=Helpers.req_header_bypass(), json_object=body, ) if Helpers.code_check(r): self.device_status = 'on' if power else 'off' return True logger.warning('Error turning %s %s', self.device_name, 'on' if power else 'off') return False pyvesync-2.1.18/src/pyvesync/vesyncswitch.py000066400000000000000000000331621475441503300212500ustar00rootroot00000000000000"""Classes for VeSync Switch Devices. This module provides classes for VeSync Switch Devices: 1. VeSyncSwitch: Abstract Base class for VeSync Switch Devices. 2. VeSyncWallSwitch: Class for VeSync Wall Switch Devices ESWL01 and ESWL03. 3. VeSyncDimmerSwitch: Class for VeSync Dimmer Switch Devices ESWD16. Attributes: feature_dict (dict): Dictionary of switch models and their supported features. Defines the class to use for each switch model and the list of features switch_modules (dict): Dictionary of switch models as keys and their associated classes as string values. Note: The switch device is built from the `feature_dict` dictionary and used by the `vesync.object_factory` during initial call to pyvesync.vesync.update() and determines the class to instantiate for each switch model. These classes should not be instantiated manually. """ import logging import json from abc import ABCMeta, abstractmethod from typing import Dict, Union, Optional from pyvesync.helpers import Helpers as helpers from pyvesync.vesyncbasedevice import VeSyncBaseDevice logger = logging.getLogger(__name__) # --8<-- [start:feature_dict] feature_dict: Dict[str, Dict[str, Union[list, str]]] = { 'ESWL01': { 'module': 'VeSyncWallSwitch', 'features': [] }, 'ESWD16': { 'module': 'VeSyncDimmerSwitch', 'features': ['dimmable'] }, 'ESWL03': { 'module': 'VeSyncWallSwitch', 'features': [] } } # --8<-- [end:feature_dict] switch_modules: dict = {k: v['module'] for k, v in feature_dict.items()} __all__: list = list(switch_modules.values()) + ['switch_modules'] class VeSyncSwitch(VeSyncBaseDevice): """Etekcity Switch Base Class. Abstract Base Class for Etekcity Switch Devices, inherting from pyvesync.vesyncbasedevice.VeSyncBaseDevice. Should not be instantiated directly, subclassed by VeSyncWallSwitch and VeSyncDimmerSwitch. Attributes: features (list): List of features supported by the switch device. details (dict): Dictionary of switch device details. """ __metaclasss__ = ABCMeta def __init__(self, details, manager): """Initialize Switch Base Class.""" super().__init__(details, manager) self.features = feature_dict.get(self.device_type, {}).get('features') if self.features is None: logger.error('% device configuration not set', self.device_name) raise KeyError(f'Device configuration not set {self.device_name}') self.details = {} def is_dimmable(self) -> bool: """Return True if switch is dimmable.""" return bool('dimmable' in self.features) @abstractmethod def get_details(self) -> None: """Get Device Details.""" @abstractmethod def turn_on(self) -> bool: """Turn Switch On.""" @abstractmethod def turn_off(self) -> bool: """Turn switch off.""" @abstractmethod def get_config(self) -> None: """Get configuration and firmware deatils.""" @property def active_time(self) -> int: """Get active time of switch.""" return self.details.get('active_time', 0) def update(self) -> None: """Update device details.""" self.get_details() class VeSyncWallSwitch(VeSyncSwitch): """Etekcity standard wall switch class.""" def __init__(self, details, manager): """Initialize standard etekcity wall switch class.""" super().__init__(details, manager) def get_details(self) -> None: """Get switch device details.""" body = helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/inwallswitch/v1/device/devicedetail', 'post', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = r.get('deviceStatus', self.device_status) self.details['active_time'] = r.get('activeTime', 0) self.connection_status = r.get( 'connectionStatus', self.connection_status) else: logger.debug('Error getting %s details', self.device_name) def get_config(self) -> None: """Get switch device configuration info.""" body = helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = helpers.call_api( '/inwallswitch/v1/device/configurations', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.config = helpers.build_config_dict(r) else: logger.warning('Unable to get %s config info', self.device_name) def turn_off(self) -> bool: """Turn off switch device.""" body = helpers.req_body(self.manager, 'devicestatus') body['status'] = 'off' body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/inwallswitch/v1/device/devicestatus', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = 'off' return True logger.warning('Error turning %s off', self.device_name) return False def turn_on(self) -> bool: """Turn on switch device.""" body = helpers.req_body(self.manager, 'devicestatus') body['status'] = 'on' body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/inwallswitch/v1/device/devicestatus', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = 'on' return True logger.warning('Error turning %s on', self.device_name) return False class VeSyncDimmerSwitch(VeSyncSwitch): """Vesync Dimmer Switch Class with RGB Faceplate.""" def __init__(self, details, manager): """Initilize dimmer switch class.""" super().__init__(details, manager) self._brightness = 0 self._rgb_value = {'red': 0, 'blue': 0, 'green': 0} self._rgb_status = 'unknown' self._indicator_light = 'unknown' def get_details(self) -> None: """Get dimmer switch details.""" body = helpers.req_body(self.manager, 'devicedetail') body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/dimmer/v1/device/devicedetail', 'post', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = r.get('deviceStatus', self.device_status) self.details['active_time'] = r.get('activeTime', 0) self.connection_status = r.get( 'connectionStatus', self.connection_status) self._brightness = r.get('brightness') self._rgb_status = r.get('rgbStatus') self._rgb_value = r.get('rgbValue') self._indicator_light = r.get('indicatorlightStatus') else: logger.debug('Error getting %s details', self.device_name) @property def brightness(self) -> float: """Return brightness in percent.""" return self._brightness @property def indicator_light_status(self) -> str: """Faceplate brightness light status.""" return self._indicator_light @property def rgb_light_status(self) -> str: """RGB Faceplate light status.""" return self._rgb_status @property def rgb_light_value(self) -> dict: """RGB Light Values.""" return self._rgb_value def switch_toggle(self, status: str) -> bool: """Toggle switch status.""" if status not in ['on', 'off']: logger.debug('Invalid status passed to wall switch') return False body = helpers.req_body(self.manager, 'devicestatus') body['status'] = status body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/dimmer/v1/device/devicestatus', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = status return True logger.warning('Error turning %s %s', self.device_name, status) return False def turn_on(self) -> bool: """Turn switch on.""" return self.switch_toggle('on') def turn_off(self) -> bool: """Turn switch off.""" return self.switch_toggle('off') def indicator_light_toggle(self, status: str) -> bool: """Toggle indicator light.""" if status not in ['on', 'off']: logger.debug('Invalid status for wall switch') return False body = helpers.req_body(self.manager, 'devicestatus') body['status'] = status body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/dimmer/v1/device/indicatorlightstatus', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self.device_status = status return True logger.warning('Error turning %s indicator light %s', self.device_name, status) return False def indicator_light_on(self) -> bool: """Turn Indicator light on.""" return self.indicator_light_toggle('on') def indicator_light_off(self) -> bool: """Turn indicator light off.""" return self.indicator_light_toggle('off') def rgb_color_status(self, status: str, red: Optional[int] = None, blue: Optional[int] = None, green: Optional[int] = None) -> bool: """Set faceplate RGB color.""" body = helpers.req_body(self.manager, 'devicestatus') body['status'] = status body['uuid'] = self.uuid head = helpers.req_headers(self.manager) if red is not None and blue is not None and green is not None: body['rgbValue'] = {'red': red, 'blue': blue, 'green': green} r, _ = helpers.call_api( '/dimmer/v1/device/devicergbstatus', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self._rgb_status = status if body.get('rgbValue') is not None: self._rgb_value = {'red': red, 'blue': blue, 'green': green} return True logger.warning('Error turning %s off', self.device_name) return False def rgb_color_off(self) -> bool: """Turn RGB Color Off.""" return self.rgb_color_status('off') def rgb_color_on(self) -> bool: """Turn RGB Color Off.""" return self.rgb_color_status('on') def rgb_color_set(self, red: int, green: int, blue: int) -> bool: """Set RGB color of faceplate.""" try: red = int(red) green = int(green) blue = int(blue) except ValueError: return False if isinstance(red, int) and isinstance( green, int) and isinstance(blue, int): for color in [red, green, blue]: if color < 0 or color > 255: logger.warning('Invalid RGB value') return False return bool(self.rgb_color_status('on', red, green, blue)) return False def set_brightness(self, brightness: int) -> bool: """Set brightness of dimmer - 1 - 100.""" if isinstance(brightness, int) and ( brightness > 0 or brightness <= 100): body = helpers.req_body(self.manager, 'devicestatus') body['brightness'] = brightness body['uuid'] = self.uuid head = helpers.req_headers(self.manager) r, _ = helpers.call_api( '/dimmer/v1/device/updatebrightness', 'put', headers=head, json_object=body ) if r is not None and helpers.code_check(r): self._brightness = brightness return True logger.warning('Error setting %s brightness', self.device_name) else: logger.warning('Invalid brightness') return False def displayJSON(self) -> str: """JSON API for dimmer switch.""" sup_val = json.loads(super().displayJSON()) if self.is_dimmable is True: # pylint: disable=using-constant-test sup_val.update( { 'Indicator Light': str(self.active_time), 'Brightness': str(self._brightness), 'RGB Light': str(self._rgb_status), } ) return json.dumps(sup_val, indent=4) def get_config(self) -> None: """Get dimmable switch device configuration info.""" body = helpers.req_body(self.manager, 'devicedetail') body['method'] = 'configurations' body['uuid'] = self.uuid r, _ = helpers.call_api( '/dimmer/v1/device/configurations', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) if helpers.code_check(r): self.config = helpers.build_config_dict(r) else: logger.warning('Unable to get %s config info', self.device_name) pyvesync-2.1.18/src/tests/000077500000000000000000000000001475441503300154225ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/README.md000066400000000000000000000306021475441503300167020ustar00rootroot00000000000000# The pyvesync testing library This is the testing suite for the pyvesync library. Each device that is added must include tests. This helps to maintain the consistency of the API as new devices are added and the backend is refactored. I've built a relatively simple framework to ease the burden of writing tests. There are some old tests that I had previously written that I've kept as I build the new framework but these tests were not comprehensive or portable. The files that begin with `test_x_` are these previous tests and can safely be ignored. The tests primarily run each API call for each device and record the request details in YAML files in the `tests\API` directory. These files are then used to verify the request when the test is run again. The structure of the framework is as follows: 1. `call_json.py` - This file contains general functions and the device list builder. This file does not need to be edited when adding a device. 2. `call_json_DEVICE.py` - This file contains device specific responses such as the `get_details()` response and specific method responses. This file pulls in the device type list from each module. The minimum addition is to add the appropriate response to the `DeviceDetails` class and the device type associated with that response in the `DETAILS_RESPONSES` dictionary. This file also contains the `DeviceDefaults` class which are specific to the device. 3. `test_DEVICE.py` - Each module in pyvesync has it's own test module, typically with one class that inherits the `utils.BaseTest` class. The class has two methods - `test_details()` and `test_methods()` that are parametrized by `utils.pytest_generate_tests` 4. `utils.py` - Contains the general default values for all devices in the `Defaults` class and the `TestBase` class that contains the fixture that instantiates the VS object and patches the `call_api()` method. 5. `conftest.py` - Contains the `pytest_generate_tests` function that is used to parametrize the tests based on all device types listed in the respective modules. ## Running the tests There are two pytest command line arguments built into the tests to specify when to write the api data to YAML files or when to overwrite the existing API calls in the YAML files. To run a tests for development on existing devices or if you are not ready to write the api calls yet: ```bash # Through pytest pytest # or through tox tox -e testenv # you can also use the environments lint, pylint, mypy ``` If developing a new device and it is completed and thoroughly tested, pass the `--write_api` to pytest. Be sure to include the `--` before the argument in the tox command. ```bash pytest --write_api tox -e testenv -- --write_api ``` If fixing an existing device where the API call was incorrect or the api has changed, pass `--write_api` and `overwrite` to pytest. Both arguments need to be provided to overwrite existing API data already in the YAML files. ```bash pytest --write_api --overwrite tox -e testenv -- --write_api --overwrite ``` ## Testing Process The first test run verifies that all of the devices defined in each pyvesync module have a corresponding response in each `call_json_DEVICE` module. This verifies that when a new device is added, a corresponding response is added to be tested. The testing framework takes the approach of verifying the response and request of each API call separately. The request side of the call is verified by recording the request for a mocked call. The requests are recorded into YAML files in the `api` folder of the tests directory, grouped in folders by module and file by device type. The response side of the API is tested through the use of responses that have been documented in the `call_json` files and the values specified in the `Defaults` and `DeviceDefaults` classes. ## File Structure ### Device Responses and Details The call_json files contain all of the response data for each device type. The following call_json files are included in the test directory: - `call_json.py` - general API responses, including `login()` and `get_devices()`. The device list from the `get_devices()` can be used to create the device list response for all devices. - `call_json_outlets.py` - API responses for the outlets - `call_json_switches.py` - API responses for the switches - `call_json_fans.py` - API responses for the fans - `call_json_bulbs.py` - API responses for the bulbs #### call_json.py The `call_json.py` file contains the functions to build the `get_devices()` response containing the device list and each item on the device list. The `DeviceList` class contains the `device_list_response(devices_types=None, _types=None)` method which returns the full device list response based on the defined device types (model number(s)) or types (outlets, fans, etc.). The `device_list_item(model)` builds the individual device list item that is used to instantiate the device object. The default values for device configuration values are pulled from the `Defaults` class in the `utils.py` module for consistency. #### call_json_DEVICE.py Each device module has it's own `call_json` file. The structure of the files maintains a consistency for easy test definition. The `DeviceDetails` (SwitchDetails, OutletDetails) class contains the `get_details()` responses for each device as a class attribute. The name of the class attribute does not matter. The `DETAILS_RESPONSES` dictionary contains the device type as the key and references the `DeviceDetails` class attribute as the value. The `DETAILS_RESPONSES` dictionary is used to lookup the appropriate response for each device type. The responses for device methods are also defined in the `call_json_DEVICE` module. The METHOD_RESPONSES dictionary uses a defaultdict imported from `utils.py` with a simple `{"code": 0, "message": ""}` as the default value. The `METHOD_RESPONSES` dictionary is created with keys of device type and values as the defaultdict object. From here the method responses can be added to the defaultdict object for specific scenarios. ```python from utils import FunctionResponses from copy import deepcopy device_types = ['dev1', 'dev2'] # defaultdict with default value - ({"code": 0, "msg": None}, 200) method_response = FunctionResponses # Use deepcopy to build the device response dictionary used to test the get_details() method device_responses = {dev_type: deepcopy(method_response) for dev_type in device_types} # Define response for specific device & method # All response must be tuples with (json response, 200) device_responses['dev1']['special_method'] = ({'response': 'special response', 'msg': 'special method'}, 200) # The default factory can be change for a single device type since deepcopy is used. device_responses['dev2'].default_factory = lambda: ({'new_code': 0, 'msg': 'success', {'payload': {}}}, 200) ``` The method responses can also be a function that accept one argument that contains the kwargs used in the method call. This allows for more complex responses based on the method call. The test will know whether it is a straight value or function and call it accordingly. For example, this is the set status response of the valceno bulb: ```python METHOD_RESPONSES = {k: deepcopy(FunctionResponses) for k in BULBS} def valceno_set_status_response(kwargs=None): default_resp = { "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "enabled": "on", "colorMode": "hsv", "brightness": Defaults.brightness, "colorTemp": Defaults.color_temp, "hue": Defaults.color.hsv.hue*27.7778, "saturation": Defaults.color.hsv.saturation*100, "value": Defaults.color.hsv.value } } } if kwargs is not None and isinstance(kwargs, dict): if kwargs.get('hue') is not None: default_resp['result']['result']['hue'] = kwargs['hue'] * 27.7778 if kwargs.get('saturation') is not None: default_resp['result']['result']['saturation'] = kwargs['saturation'] * 100 if kwargs.get('value') is not None: default_resp['result']['result']['value'] = kwargs['value'] return default_resp, 200 XYD0001_RESP = { 'set_brightness': valceno_set_status_response, 'set_color_temp': valceno_set_status_response, 'set_hsv': valceno_set_status_response, 'set_rgb': valceno_set_status_response, } METHOD_RESPONSES['XYD0001'].update(XYD0001_RESP) ``` ### **`api`** directory with `YAML` files API requests recorded from the mocked `call_api()` method. The `api` directory contains folders for each module and files for each device_type. The structure of the YAML files is: **File** `tests/api/switches/esl100.yaml` ```yaml turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' switchNo: 3 timeZone: America/New_York token: sample_tk uuid: ESO15-TB-UUID method: put url: /outdoorsocket15a/v1/device/devicestatus ``` ### **`utils.py`** - utility functions and default value factory for tests. The `utils.py` file contains several helper functions and classes: ### Default values for API responses and requests The recorded requests are automatically scrubbed with these default values to remove sensitive information and normalize the data. Any new API calls added to `call_json_` files should use the defaults values wherever possible. ```python from utils import Defaults # Default Class variables token = Defaults.token account_id = Defaults.account_id trace_id = Defaults.trace_id active_time = Defaults.active_time # The default Color dataclass contains the attributes red, green, blue, hue saturation & value. Conversion is automatically done regardless of the input color model. This is to normalize any API calls that involve changing color color = Color(red=50, green=100, blue=255) brightness = Defaults.brightness color_temp = Defaults.color_temp # Default values that use methods device_name = Defaults.name(dev_type="ESL100") # returns 'ESL100-NAME' device_cid = Defaults.cid(dev_type="ESL100") # returns 'ESL100-CID' device_uuid = Defaults.uuid(dev_type="ESL100") # returns 'ESL100-UUID' device_mac = Defaults.macid(dev_type="ESL100") # returns 'ESL100-MACID' ``` The `utils` module contains the base class with a fixture that instantiates the VeSync object and patches `call_api()` automatically, allowing a return value to be set.. ```python from utils import TestBase, FunctionResponses class TestDevice(TestBase): def test_details(self): vesync_instance = self.manager mock_api_object = self.mock_api # patch('pyvesync.helpers.call_api', autopspec=True) mock_api_object.return_value = FunctionResponses['default'] caplog = self.caplog assert vesync_instance.enabled is True ``` ## Test Structure Each module in the pyvesync library has an associated testing module, for example, `vesyncswitches` and `test_switches`. Most testing modules have one class, except for the `test_fans` module, which has separate classes for humidifiers and air purifiers. The class inherits from the `TestBase` class in `utils.py` and is parametrized by `pytest_generate_tests` based on the method. The parameters are defined by the class attributes. The `base_methods` and `device_methods` class attributes define the method and arguments in a list of lists with the first item, the method name and the second optional item, the method kwargs. The `base_methods` class attribute defines methods that are common to all devices. The `device_methods` class attribute defines methods that are specific to the device type. This is an examples of the class definition: ```python from utils import TestBase class TestBulbs(TestBase): device = 'bulbs' bulbs = call_json_bulbs.BULBS base_methods = [['turn_on'], ['turn_off'], ['set_brightness', {'brightness': 50}]] device_methods = { 'ESL100CW': [['set_color_temp', {'color_temp': 50}]] } ``` The methods are then parametrized based on those values. For most device additions, the only thing that needs to be added is the device type in the `DETAILS_RESPONSES` and possibly a response in the `METHOD_RESPONSES` dictionary. See the docstrings in the modules for more information. pyvesync-2.1.18/src/tests/api/000077500000000000000000000000001475441503300161735ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesync/000077500000000000000000000000001475441503300175025ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesync/VeSync.yaml000066400000000000000000000013211475441503300215720ustar00rootroot00000000000000get_devices: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devices pageNo: '1' pageSize: '100' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/devices login: json_object: acceptLanguage: en appVersion: 2.8.6 devToken: '' email: EMAIL method: login password: PASSWORD phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York traceId: TRACE_ID userType: '1' method: post url: /cloud/v1/user/login pyvesync-2.1.18/src/tests/api/vesyncbulb/000077500000000000000000000000001475441503300203475ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesyncbulb/ESL100.yaml000066400000000000000000000032151475441503300221000ustar00rootroot00000000000000set_brightness: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id brightNess: '50' status: 'on' timeZone: America/New_York token: sample_tk uuid: ESL100-UUID method: put url: /SmartBulb/v1/device/updateBrightness turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESL100-UUID method: put url: /SmartBulb/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESL100-UUID method: put url: /SmartBulb/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESL100-UUID method: post url: /SmartBulb/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncbulb/ESL100CW.yaml000066400000000000000000000052261475441503300223360ustar00rootroot00000000000000set_brightness: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100CW-CID configModule: ConfigModule jsonCmd: light: brightness: 50 method: bypass phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/bypass set_color_temp: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100CW-CID jsonCmd: light: colorTempe: 50 method: bypass phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/bypass turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100CW-CID configModule: ConfigModule jsonCmd: light: action: 'off' method: bypass phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/bypass turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100CW-CID configModule: ConfigModule jsonCmd: light: action: 'on' method: bypass phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/bypass update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100CW-CID configModule: ConfigModule jsonCmd: getLightStatus: get method: bypass phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v1/deviceManaged/bypass pyvesync-2.1.18/src/tests/api/vesyncbulb/ESL100MC.yaml000066400000000000000000000073041475441503300223230ustar00rootroot00000000000000enable_white_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'on' blue: 0 brightness: 100 colorMode: white green: 0 red: 0 speed: 0 method: setLightStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_brightness: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'on' blue: 0 brightness: 50 colorMode: white green: 0 red: 0 speed: 0 method: setLightStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_rgb: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'on' blue: 255 brightness: 100 colorMode: color green: 200 red: 50 speed: 0 method: setLightStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: ESL100MC-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getLightStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncbulb/XYD0001.yaml000066400000000000000000000104061475441503300222010ustar00rootroot00000000000000enable_white_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: brightness: '' colorMode: white colorTemp: '' force: 1 hue: '' saturation: '' value: '' method: setLightStatusV2 source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_brightness: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: brightness: 50 colorMode: '' colorTemp: '' force: 0 hue: '' saturation: '' value: '' method: setLightStatusV2 source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_color_temp: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: brightness: '' colorMode: white colorTemp: 50 force: 1 hue: '' saturation: '' value: '' method: setLightStatusV2 source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_hsv: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: brightness: 100 colorMode: hsv colorTemp: '' force: 1 hue: 5556 saturation: 5000 value: '' method: setLightStatusV2 source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: XYD0001-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: {} method: getLightStatusV2 source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/000077500000000000000000000000001475441503300201675ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesyncfan/Classic200S.yaml000066400000000000000000000144741475441503300230130ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setIndicatorLightSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setIndicatorLightSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Classic300S.yaml000066400000000000000000000144021475441503300230030ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Classic300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Core200S.yaml000066400000000000000000000132251475441503300223130ustar00rootroot00000000000000change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 3 mode: manual type: wind method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 clear_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 1 type: wind method: setLevel type: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'off' total: 100 method: addTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Core300S.yaml000066400000000000000000000143171475441503300223170ustar00rootroot00000000000000auto_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 3 mode: manual type: wind method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 clear_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 1 type: wind method: setLevel type: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'off' total: 100 method: addTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core300S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Core400S.yaml000066400000000000000000000143171475441503300223200ustar00rootroot00000000000000auto_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 3 mode: manual type: wind method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 clear_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 1 type: wind method: setLevel type: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'off' total: 100 method: addTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core400S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Core600S.yaml000066400000000000000000000143171475441503300223220ustar00rootroot00000000000000auto_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 3 mode: manual type: wind method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 clear_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 1 type: wind method: setLevel type: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_timer: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: action: 'off' total: 100 method: addTimer source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Core600S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/Dual200S.yaml000066400000000000000000000143411475441503300223100ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: Dual200S-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LAP-EL551S-AUS.yaml000066400000000000000000000075671475441503300230100ustar00rootroot00000000000000change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 3 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 1 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: workMode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-EL551S-AUS-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-EL551S-AUS-CID deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LAP-V102S-AASR.yaml000066400000000000000000000075671475441503300230030ustar00rootroot00000000000000change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 3 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 1 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: workMode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V102S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V102S-AASR-CID deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LAP-V201S-AASR.yaml000066400000000000000000000075671475441503300230030ustar00rootroot00000000000000change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 3 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 1 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: workMode: sleep method: setPurifierMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LAP-V201S-AASR-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LAP-V201S-AASR-CID deviceRegion: US method: bypassV2 payload: data: {} method: getPurifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LEH-S601S-WUS.yaml000066400000000000000000000156421475441503300227210ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: autoPro method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: autoPro method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_drying_mode_enabled: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: autoDryingSwitch: 0 method: setDryingMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: targetHumidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: mist virtualLevel: 2 method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: screenSwitch: 0 method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: screenSwitch: 1 method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LEH-S601S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LTF-F422S-KEU.yaml000066400000000000000000000075131475441503300226660ustar00rootroot00000000000000change_fan_speed: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: wind manualSpeedLevel: 3 method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 manual_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: workMode: normal method: setTowerFanMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 sleep_mode: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: workMode: advancedSleep method: setTowerFanMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LTF-F422S-KEU-CID configModel: ConfigModule configModule: ConfigModule debugMode: false deviceId: LTF-F422S-KEU-CID deviceRegion: US method: bypassV2 payload: data: {} method: getTowerFanStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LUH-A602S-WUSR.yaml000066400000000000000000000156041475441503300230400ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: humidity method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 set_warm_level: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 3 type: warm method: setLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-A602S-WUSR-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LUH-M101S-WUS.yaml000066400000000000000000000145201475441503300227200ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: autoStopSwitch: 0 method: setAutoStopSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: autoStopSwitch: 1 method: setAutoStopSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: targetHumidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: workMode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: levelIdx: 0 levelType: mist virtualLevel: 2 method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: powerSwitch: 0 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: screenSwitch: 0 method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: powerSwitch: 1 switchIdx: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: screenSwitch: 1 method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-M101S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LUH-O451S-WEU.yaml000066400000000000000000000144301475441503300227140ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WEU-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LUH-O451S-WUS.yaml000066400000000000000000000144301475441503300227320ustar00rootroot00000000000000automatic_stop_off: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 automatic_stop_on: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true method: setAutomaticStop source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: auto method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: target_humidity: 50 method: setTargetHumidity source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: mode: manual method: setHumidityMode source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: id: 0 level: 2 type: mist method: setVirtualLevel source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: false id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: false method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: enabled: true id: 0 method: setSwitch source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: state: true method: setDisplay source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: LUH-O451S-WUS-CID configModule: ConfigModule debugMode: false deviceRegion: US method: bypassV2 payload: data: {} method: getHumidifierStatus source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncfan/LV-PUR131S.yaml000066400000000000000000000046521475441503300224170ustar00rootroot00000000000000change_fan_speed: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id level: 3 timeZone: America/New_York token: sample_tk uuid: LV-PUR131S-UUID method: put url: /131airPurifier/v1/device/updateSpeed manual_mode: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id level: 1 mode: manual timeZone: America/New_York token: sample_tk uuid: LV-PUR131S-UUID method: put url: /131airPurifier/v1/device/updateMode sleep_mode: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id mode: sleep timeZone: America/New_York token: sample_tk uuid: LV-PUR131S-UUID method: put url: /131airPurifier/v1/device/updateMode turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: LV-PUR131S-UUID method: put url: /131airPurifier/v1/device/deviceStatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: LV-PUR131S-UUID method: put url: /131airPurifier/v1/device/deviceStatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: LV-PUR131S-UUID method: post url: /131airPurifier/v1/device/deviceDetail pyvesync-2.1.18/src/tests/api/vesyncoutlet/000077500000000000000000000000001475441503300207375ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesyncoutlet/BSDOG01.yaml000066400000000000000000000041501475441503300226220ustar00rootroot00000000000000get_details: headers: Content-Type: application/json; charset=UTF-8 User-Agent: okhttp/3.12.1 json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 cid: BSDOG01-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: {} method: getProperty source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: BSDOG01-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: powerSwitch_1: 0 method: setProperty source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: BSDOG01-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: powerSwitch_1: 1 method: setProperty source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID 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: 2.8.6 cid: BSDOG01-CID configModule: ConfigModule deviceRegion: US method: bypassV2 payload: data: {} method: getProperty source: APP phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID method: post url: /cloud/v2/deviceManaged/bypassV2 pyvesync-2.1.18/src/tests/api/vesyncoutlet/ESO15-TB.yaml000066400000000000000000000065431475441503300227320ustar00rootroot00000000000000get_details: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESO15-TB-UUID method: post url: /outdoorsocket15a/v1/device/devicedetail get_monthly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energymonth mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESO15-TB-UUID method: post url: /outdoorsocket15a/v1/device/energymonth get_weekly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyweek mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESO15-TB-UUID method: post url: /outdoorsocket15a/v1/device/energyweek get_yearly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyyear mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESO15-TB-UUID method: post url: /outdoorsocket15a/v1/device/energyyear turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' switchNo: 1 timeZone: America/New_York token: sample_tk uuid: ESO15-TB-UUID method: put url: /outdoorsocket15a/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' switchNo: 1 timeZone: America/New_York token: sample_tk uuid: ESO15-TB-UUID method: put url: /outdoorsocket15a/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESO15-TB-UUID method: post url: /outdoorsocket15a/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncoutlet/ESW01-EU.yaml000066400000000000000000000063501475441503300227350ustar00rootroot00000000000000get_details: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW01-EU-UUID method: post url: /10a/v1/device/devicedetail get_monthly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energymonth mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW01-EU-UUID method: post url: /10a/v1/device/energymonth get_weekly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyweek mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW01-EU-UUID method: post url: /10a/v1/device/energyweek get_yearly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyyear mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW01-EU-UUID method: post url: /10a/v1/device/energyyear turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESW01-EU-UUID method: put url: /10a/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESW01-EU-UUID method: put url: /10a/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW01-EU-UUID method: post url: /10a/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncoutlet/ESW03-USA.yaml000066400000000000000000000063571475441503300230650ustar00rootroot00000000000000get_details: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW03-USA-UUID method: post url: /10a/v1/device/devicedetail get_monthly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energymonth mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW03-USA-UUID method: post url: /10a/v1/device/energymonth get_weekly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyweek mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW03-USA-UUID method: post url: /10a/v1/device/energyweek get_yearly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyyear mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW03-USA-UUID method: post url: /10a/v1/device/energyyear turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESW03-USA-UUID method: put url: /10a/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESW03-USA-UUID method: put url: /10a/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW03-USA-UUID method: post url: /10a/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncoutlet/ESW15-USA.yaml000066400000000000000000000077721475441503300230720ustar00rootroot00000000000000get_details: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW15-USA-UUID method: post url: /15a/v1/device/devicedetail get_monthly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energymonth mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW15-USA-UUID method: post url: /15a/v1/device/energymonth get_weekly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyweek mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW15-USA-UUID method: post url: /15a/v1/device/energyweek get_yearly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: energyyear mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW15-USA-UUID method: post url: /15a/v1/device/energyyear turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESW15-USA-UUID method: put url: /15a/v1/device/devicestatus turn_off_nightlight: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id mode: manual timeZone: America/New_York token: sample_tk uuid: ESW15-USA-UUID method: put url: /15a/v1/device/nightlightstatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESW15-USA-UUID method: put url: /15a/v1/device/devicestatus turn_on_nightlight: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id mode: auto timeZone: America/New_York token: sample_tk uuid: ESW15-USA-UUID method: put url: /15a/v1/device/nightlightstatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESW15-USA-UUID method: post url: /15a/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncoutlet/wifi-switch-1.3.yaml000066400000000000000000000032101475441503300243530ustar00rootroot00000000000000get_details: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: get url: /v1/device/wifi-switch-1.3-CID/detail get_monthly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: get url: /v1/device/wifi-switch-1.3-CID/energy/month get_weekly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: get url: /v1/device/wifi-switch-1.3-CID/energy/week get_yearly_energy: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: get url: /v1/device/wifi-switch-1.3-CID/energy/year turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: put url: /v1/wifi-switch-1.3/wifi-switch-1.3-CID/status/off turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: put url: /v1/wifi-switch-1.3/wifi-switch-1.3-CID/status/on update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York method: get url: /v1/device/wifi-switch-1.3-CID/detail pyvesync-2.1.18/src/tests/api/vesyncswitch/000077500000000000000000000000001475441503300207245ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/api/vesyncswitch/ESWD16.yaml000066400000000000000000000054651475441503300225330ustar00rootroot00000000000000indicator_light_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/indicatorlightstatus rgb_color_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/devicergbstatus rgb_color_set: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id rgbValue: blue: 100 green: 225 red: 50 status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/devicergbstatus set_brightness: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id brightness: 100 timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/updatebrightness turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWD16-UUID method: put url: /dimmer/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESWD16-UUID method: post url: /dimmer/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncswitch/ESWL01.yaml000066400000000000000000000023741475441503300225310ustar00rootroot00000000000000turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESWL01-UUID method: put url: /inwallswitch/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWL01-UUID method: put url: /inwallswitch/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESWL01-UUID method: post url: /inwallswitch/v1/device/devicedetail pyvesync-2.1.18/src/tests/api/vesyncswitch/ESWL03.yaml000066400000000000000000000023741475441503300225330ustar00rootroot00000000000000turn_off: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'off' timeZone: America/New_York token: sample_tk uuid: ESWL03-UUID method: put url: /inwallswitch/v1/device/devicestatus turn_on: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id status: 'on' timeZone: America/New_York token: sample_tk uuid: ESWL03-UUID method: put url: /inwallswitch/v1/device/devicestatus update: headers: accept-language: en accountId: sample_id appVersion: 2.8.6 content-type: application/json tk: sample_tk tz: America/New_York json_object: acceptLanguage: en accountID: sample_id appVersion: 2.8.6 method: devicedetail mobileId: '1234567890123456' phoneBrand: SM N9005 phoneOS: Android timeZone: America/New_York token: sample_tk traceId: TRACE_ID uuid: ESWL03-UUID method: post url: /inwallswitch/v1/device/devicedetail pyvesync-2.1.18/src/tests/call_json.py000066400000000000000000000451011475441503300177410ustar00rootroot00000000000000import copy import pyvesync.helpers as helpers from utils import Defaults import call_json_switches import call_json_outlets import call_json_bulbs import call_json_fans API_BASE_URL = helpers.API_BASE_URL API_RATE_LIMIT = helpers.API_RATE_LIMIT API_TIMEOUT = helpers.API_TIMEOUT DEFAULT_TZ = helpers.DEFAULT_TZ APP_VERSION = helpers.APP_VERSION PHONE_BRAND = helpers.PHONE_BRAND PHONE_OS = helpers.PHONE_OS MOBILE_ID = helpers.MOBILE_ID USER_TYPE = helpers.USER_TYPE """ DEFAULT_BODY = Standard body for new device calls DEFAULT_HEADER = standard header for most calls DEFAULT_HEADER_BYPASS = standard header for most calls api V2 ENERGY_HISTORY = standard outlet energy history response ------------------------------------------------------- login_call_body(email, pass) = body of login call LOGIN_RET_BODY = return of login call ------------------------------------------------------- get_devices_body() = body of call to get device list LIST_CONF_10AEU = device list entry for 10A Europe outlet LIST_CONF_10AUS = devlice list entry for 10A US outlet LIST_CONF_7A = device list entry for 7A outlet LIST_CONF_AIR = device list entry for air purifier LIST_CONF_15A = device list entry for 15A outlet LIST_CONF_WS = device list entry for wall switch LIST_CONF_ESL100 = device list entry for bulb ESL100 LIST_CONF_OUTDOOR_1 = devlice list entry for outdoor outlet subDevice 1 LIST_CONF_OUTDOOR_2 = devlice list entry for outdoor outlet subDevice 2 DEVLIST_ALL = Return tuple for all devices DEVLIST_10AEU = device list return for only 10A eu outlet DEVLIST_10AUS = device list return for only 10A us outlet DEVLIST_7A = device list return for only 7A outlet DEVLIST_15A = device list return for only 15A outlet DEVLIST_WS = device list return for only wall switch DEVLIST_AIR = device list return for just air purifier DEVLIST_ESL100 = device list return for just ESL100 bulb DEVLIST_OUTDOOR_1 = device list return for outdoor outlet subDevice 1 DEVLIST_OUTDOOR_2 = device list return for outdoor outlet subDevice 2 --------------------------------------------------------- DETAILS_10A = Return for 10A outlet device details DETAILS_15A = Return for 15A outlet device details DETAILS_7A = Return for 7A outlet device details DETAILS_WS = Return for wall switch device details DETAILS_AIR = Return for Air Purifier device details DETAILS_ESL100 = Return for ESL100 Bulb device details DETAILS_OUTDOOR = return for 2 plug outdoor outlet """ BULBS = call_json_bulbs.BULBS FANS = call_json_fans.FANS OUTLETS = call_json_outlets.OUTLETS SWITCHES = call_json_switches.SWITCHES DEFAULT_HEADER = { 'accept-language': 'en', 'accountId': Defaults.account_id, 'appVersion': APP_VERSION, 'content-type': 'application/json', 'tk': Defaults.token, 'tz': DEFAULT_TZ, } DEFAULT_HEADER_BYPASS = { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': 'okhttp/3.12.1' } def BYPASS_V1_BODY(cid: str, config_module: str, json_cmd: dict): return { "traceId": Defaults.trace_id, "method": "bypass", "token": Defaults.token, "accountID": Defaults.account_id, "timeZone": DEFAULT_TZ, "acceptLanguage": "en", "appVersion": APP_VERSION, "phoneBrand": PHONE_BRAND, "phoneOS": PHONE_OS, "cid": cid, "configModule": config_module, "jsonCmd": json_cmd } DEFAULT_BODY = { 'acceptLanguage': 'en', 'accountID': Defaults.account_id, 'appVersion': APP_VERSION, 'pageNo': 1, 'pageSize': 100, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'timeZone': DEFAULT_TZ, 'token': Defaults.token, 'traceId': Defaults.trace_id, } LOGIN_RET_BODY = ( { 'traceId': Defaults.trace_id, 'msg': '', 'result': { 'accountID': Defaults.account_id, 'avatarIcon': '', 'acceptLanguage': 'en', 'gdprStatus': True, 'nickName': 'nick', 'userType': '1', 'token': Defaults.token, }, 'code': 0, }, 200, ) def login_call_body(email, password): json_object = { 'acceptLanguage': 'en', 'appVersion': APP_VERSION, 'devToken': '', 'email': email, 'method': 'login', 'password': password, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'timeZone': DEFAULT_TZ, 'traceId': Defaults.trace_id, 'userType': '1', } return json_object class DeviceList: list_response_base = { 'code': 0, 'msg': 'Success', 'result': { 'pageNo': 1, 'pageSize': 100, 'total': 0, 'list': [], } } device_list_base = { 'extension': None, 'isOwner': True, 'authKey': None, 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'mode': None, 'speed': None, 'deviceProps': None, 'configModule': 'ConfigModule', } bulbs = dict.fromkeys(call_json_bulbs.BULBS, "wifi-light") outlets = dict.fromkeys(call_json_outlets.OUTLETS, "wifi-switch") fans = dict.fromkeys(call_json_fans.FANS, "wifi-air") switches = dict.fromkeys(call_json_switches.SWITCHES, "Switches") @classmethod def device_list_item(cls, model, sub_device_no=0): model_types = {**cls.bulbs, **cls.outlets, **cls.fans, **cls.switches} device_dict = cls.device_list_base model_dict = device_dict.copy() model_dict['deviceType'] = model model_dict['deviceName'] = Defaults.name(model) model_dict['type'] = model_types.get(model) model_dict['cid'] = Defaults.cid(model) model_dict['uuid'] = Defaults.uuid(model) model_dict['macID'] = Defaults.macid(model) if model == 'ESO15-TB': model_dict['subDeviceNo'] = 1 return model_dict @classmethod def device_list_response(cls, device_types=None, _types=None): """Class method that returns the api get_devices response Args: _types (list, str, optional): Can be one or list of types of devices. Defaults to None. can be bulb, fans, switches, outlets in list or string device_types (list, str optional): List or string of device_type(s) to return. Defaults to None. """ response_base = copy.deepcopy(cls.list_response_base) if _types is not None: if isinstance(_types, list): full_model_list = {} for _type in _types: device_types = full_model_list.update(cls.__dict__[_type]) else: full_model_list = cls.__dict__[_types] else: full_model_list = {**cls.bulbs, **cls.outlets, **cls.fans, **cls.switches} if device_types is not None: if isinstance(device_types, list): full_model_list = {k: v for k, v in full_model_list.items() if k in device_types} else: full_model_list = {k: v for k, v in full_model_list.items() if k == device_types} for model in full_model_list: response_base['result']['list'].append(cls.device_list_item(model)) response_base['result']['total'] += 1 return response_base, 200 LIST_CONF_7A = { 'deviceType': 'wifi-switch-1.3', 'extension': None, 'macID': None, 'type': 'wifi-switch', 'deviceName': 'Name 7A Outlet', 'connectionType': 'wifi', 'uuid': None, 'speed': None, 'deviceStatus': 'on', 'mode': None, 'configModule': '7AOutlet', 'currentFirmVersion': '1.95', 'connectionStatus': 'online', 'cid': '7A-CID', } LIST_CONF_15A = { 'deviceType': 'ESW15-USA', 'extension': None, 'macID': None, 'type': 'wifi-switch', 'deviceName': 'Name 15A Outlet', 'connectionType': 'wifi', 'uuid': 'UUID', 'speed': None, 'deviceStatus': 'on', 'mode': None, 'configModule': '15AOutletNightlight', 'currentFirmVersion': None, 'connectionStatus': 'online', 'cid': '15A-CID', } LIST_CONF_WS = { 'deviceType': 'ESWL01', 'extension': None, 'macID': None, 'type': 'Switches', 'deviceName': 'Name Wall Switch', 'connectionType': 'wifi', 'uuid': 'UUID', 'speed': None, 'deviceStatus': 'on', 'mode': None, 'configModule': 'InwallswitchUS', 'currentFirmVersion': None, 'connectionStatus': 'online', 'cid': 'WS-CID', } LIST_CONF_10AEU = { 'deviceType': 'ESW01-EU', 'extension': None, 'macID': None, 'type': 'wifi-switch', 'deviceName': 'Name 10A Outlet', 'connectionType': 'wifi', 'uuid': 'UUID', 'speed': None, 'deviceStatus': 'on', 'mode': None, 'configModule': '10AOutletEU', 'currentFirmVersion': None, 'connectionStatus': 'online', 'cid': '10A-CID', } LIST_CONF_10AUS = { 'deviceType': 'ESW03-USA', 'extension': None, 'macID': None, 'type': 'wifi-switch', 'deviceName': 'Name 10A Outlet', 'connectionType': 'wifi', 'uuid': 'UUID', 'speed': None, 'deviceStatus': 'on', 'mode': None, 'configModule': '10AOutletUSA', 'currentFirmVersion': None, 'connectionStatus': 'online', 'cid': '10A-CID', } LIST_CONF_OUTDOOR_1 = { 'deviceRegion': 'US', 'deviceName': 'Outdoor Socket B', 'cid': 'OUTDOOR-CID', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'ESO15-TB', 'type': 'wifi-switch', 'uuid': 'UUID', 'configModule': 'OutdoorSocket15A', 'macID': None, 'mode': None, 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': 1, } LIST_CONF_OUTDOOR_2 = { 'deviceRegion': 'US', 'deviceName': 'Outdoor Socket B', 'cid': 'OUTDOOR-CID', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'ESO15-TB', 'type': 'wifi-switch', 'uuid': 'UUID', 'configModule': 'OutdoorSocket15A', 'macID': None, 'mode': None, 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': 2, } LIST_CONF_ESL100 = { 'deviceRegion': 'US', 'deviceName': 'Etekcity Soft White Bulb', 'cid': 'ESL100-CID', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'ESL100', 'type': 'Wifi-light', 'uuid': 'UUID', 'configModule': 'WifiSmartBulb', 'macID': None, 'mode': None, 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': None, } LIST_CONF_ESL100CW = { 'deviceRegion': 'US', 'deviceName': 'ESL100CW NAME', 'cid': 'ESL100CW-CID', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'ESL100CW', 'type': 'Wifi-light', 'uuid': 'ESL100CW-UUID', 'configModule': 'WifiSmartBulb', 'macID': None, 'mode': None, 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': None, } LIST_CONF_AIR = { 'deviceName': 'Name Air Purifier', 'cid': 'AIRPUR-CID', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'LV-PUR131S', 'type': 'wifi-air', 'uuid': 'UUID', 'configModule': 'AirPurifier131', 'macID': None, 'mode': 'manual', 'speed': 'low', 'extension': None, 'currentFirmVersion': None, } LIST_CONF_DUAL200S = { 'deviceRegion': 'EU', 'isOwner': True, 'authKey': None, 'deviceName': '200S NAME', 'cid': 'CID-200S', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'WiFi+BTOnboarding+BTNotify', 'deviceType': 'LUH-D301S-WEU', 'type': 'wifi-air', 'uuid': 'UUID-200S', 'configModule': 'WFON_AHM_LUH-D301S-WEU_EU', 'macID': None, 'mode': None, 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': None, 'subDeviceType': None, } LIST_CONF_DIMMER = { "deviceRegion": "US", "deviceName": "Etekcity Dimmer Switch", "cid": "DIM-CID", "deviceStatus": "on", "connectionStatus": "online", "connectionType": "wifi", "deviceType": "ESWD16", "type": "Switches", "uuid": "DIM-UUID", "configModule": "WifiWallDimmer" } LIST_CONF_600S = { 'deviceRegion': 'US', 'deviceName': 'Bedroom Humidifier', 'deviceImg': '', 'cid': 'CID-600S', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'WiFi+BTOnboarding+BTNotify', 'deviceType': 'LUH-A602S-WUS', 'type': 'wifi-air', 'uuid': 'UUID-600S', 'configModule': 'WFON_AHM_LUH-A602S-WUS_US', 'macID': None, 'subDeviceNo': None, 'subDeviceType': None, 'deviceProp': None } LIST_CONF_ESL100MC = { "cid": "CID-ESL100MC", "uuid": "UUID-ESL100MC", "macID": None, "subDeviceNo": 0, "subDeviceType": None, "deviceName": "ESL100MC NAME", "configModule": "WiFi_Bulb_MulticolorBulb_US", "type": "Wifi-light", "deviceType": "ESL100MC", "deviceStatus": "on", "connectionType": "wifi", "currentFirmVersion": "1.0.12", "connectionStatus": "online", "speed": None, "extension": None, "deviceProp": None } LIST_CONF_LV131S = { 'deviceName': 'LV131S NAME', 'cid': 'CID-LV131S', 'deviceStatus': 'on', 'connectionStatus': 'online', 'connectionType': 'wifi', 'deviceType': 'LV-PUR131S', 'type': 'wifi-air', 'uuid': 'UUID-LV131S', 'configModule': 'AirPurifier131', 'macID': None, 'mode': 'auto', 'speed': None, 'extension': None, 'currentFirmVersion': None, 'subDeviceNo': None, 'subDeviceType': None } LIST_CONF_VALCENO = { "deviceName": "VALCENO NAME", "cid": "CID-VALCENO", "deviceStatus": "on", "connectionStatus": "online", "connectionType": "WiFi+BTOnboarding+BTNotify", "deviceType": "XYD0001", "type": "Wifi-light", "uuid": "UUID-VALCENO", "configModule": "VC_WFON_BLB_A19-MC_US", "macID": None, "subDeviceNo": None, "subDeviceType": None } API_URL = '/cloud/v1/deviceManaged/devices' METHOD = 'POST' FULL_DEV_LIST = [ LIST_CONF_10AEU, LIST_CONF_10AUS, LIST_CONF_15A, LIST_CONF_7A, LIST_CONF_AIR, LIST_CONF_WS, LIST_CONF_ESL100, LIST_CONF_OUTDOOR_1, LIST_CONF_OUTDOOR_2, LIST_CONF_DIMMER, LIST_CONF_600S, LIST_CONF_LV131S, LIST_CONF_DUAL200S, LIST_CONF_ESL100CW, LIST_CONF_ESL100MC, LIST_CONF_VALCENO ] @classmethod def DEVICE_LIST_RETURN(cls, dev_conf: dict) -> tuple: """Test the fan.""" return ( { 'code': 0, 'result': { 'list': [dev_conf] } }, 200 ) FAN_TEST = ({'code': 0, 'result': {'list': [LIST_CONF_600S, LIST_CONF_LV131S, LIST_CONF_DUAL200S]}}, 200) DEVLIST_ALL = ({'code': 0, 'result': {'list': FULL_DEV_LIST}}, 200) DEVLIST_7A = ({'code': 0, 'result': {'list': [LIST_CONF_7A]}}, 200) DEVLIST_15A = ({'code': 0, 'result': {'list': [LIST_CONF_15A]}}, 200) DEVLIST_10AEU = ({'code': 0, 'result': {'list': [LIST_CONF_10AEU]}}, 200) DEVLIST_10AUS = ({'code': 0, 'result': {'list': [LIST_CONF_10AUS]}}, 200) DEVLIST_WS = ({'code': 0, 'result': {'list': [LIST_CONF_WS]}}, 200) DEVLIST_DIMMER = ({'code': 0, 'result': {'list': [LIST_CONF_DIMMER]}}, 200) DEVLIST_AIR = ({'code': 0, 'result': {'list': [LIST_CONF_AIR]}}, 200) DEVLIST_ESL100 = ({'code': 0, 'result': {'list': [LIST_CONF_ESL100]}}, 200) DEVLIST_DUAL200S = ({'code': 0, 'result': {'list': [LIST_CONF_DUAL200S]}}, 200) DEVLIST_OUTDOOR = ( {'code': 0, 'result': {'list': [LIST_CONF_OUTDOOR_1, LIST_CONF_OUTDOOR_2]}}, 200, ) class DeviceDetails: """Responses for get_details() method for all devices. class attributes: outlets : dict Dictionary of outlet responses for each device type. switches : dict Dictionary of switch responses for each device type. bulbs : dict Dictionary of bulb responses for each device type. fans : dict Dictionary of humidifier & air pur responses for each device type. all_devices : dict Dictionary of all device responses for each device type. Example ------- outlets = {'ESW01-EU': {'switches': [{'outlet': 0, 'switch': 'on'}]}} """ outlets = call_json_outlets.DETAILS_RESPONSES switches = call_json_switches.DETAILS_RESPONSES fans = call_json_fans.DETAILS_RESPONSES bulbs = call_json_bulbs.DETAILS_RESPONSES all_devices = { 'outlets': outlets, 'switches': switches, 'fans': fans, 'bulbs': bulbs } def get_devices_body(): """Build device body dictionary.""" body = DEFAULT_BODY body['method'] = 'devices' return body, 200 def get_details_body(): body = DEFAULT_BODY body['method'] = 'deviceDetail' return body, 200 DETAILS_BADCODE = ( { 'code': 1, 'deviceImg': '', 'activeTime': 1, 'energy': 1, 'power': '1', 'voltage': '1', }, 200, ) STATUS_BODY = { 'accountID': Defaults.account_id, 'token': Defaults.token, 'uuid': 'UUID', 'timeZone': DEFAULT_TZ, } def off_body(): body = STATUS_BODY body['status'] = 'off' return body, 200 def on_body(): body = STATUS_BODY body['status'] = 'on' return body, 200 pyvesync-2.1.18/src/tests/call_json_bulbs.py000066400000000000000000000142221475441503300211300ustar00rootroot00000000000000""" Light Bulbs Device API Responses OUTLET variable is a list of device types DETAILS_RESPONSES variable is a dictionary of responses from the API for get_details() methods. The keys are the device types and the values are the responses. The responses are tuples of (response, status) METHOD_RESPONSES variable is a defaultdict of responses from the API. This is the FunctionResponse variable from the utils module in the tests dir. The default response is a tuple with the value ({"code": 0, "msg": "success"}, 200). The values of METHOD_RESPONSES can be a function that takes a single argument or a static value. The value is checked if callable at runtime and if so, it is called with the provided argument. If not callable, the value is returned as is. METHOD_RESPONSES = { 'ESL100': defaultdict( lambda: ({"code": 0, "msg": "success"}, 200)) ) } # For a function to handle the response def status_response(request_body=None): # do work with request_body return request_body, 200 METHOD_RESPONSES['ESL100']['set_status'] = status_response # To change the default value for a device type METHOD_RESPONSES['XYD0001'].default_factory = lambda: ({"code": 0, "msg": "success"}, 200) """ from copy import deepcopy from utils import FunctionResponses, Defaults from pyvesync.vesyncbulb import bulb_modules TRACE_ID = "TRACE_ID" # BULBS = ['ESL100', 'ESL100CW', 'ESL100MC', 'XYD0001'] BULBS = bulb_modules.keys() BULBS_NUM = len(BULBS) class BulbDetails: details_esl100 = ({ 'code': 0, 'msg': None, 'deviceStatus': 'on', 'connectionStatus': 'online', 'name': Defaults.name('ESL100'), 'brightNess': Defaults.brightness, 'timer': None, 'away': None, 'schedule': None, 'ownerShip': '1', 'scheduleCount': 0, }, 200) details_esl100cw = ({ "traceId": TRACE_ID, "code": 0, "msg": None, "module": None, "stacktrace": None, "result": { "light": { "action": "on", "brightness": Defaults.brightness, "colorTempe": Defaults.color_temp, } } }, 200) details_esl100mc = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "action": "on", "brightness": Defaults.brightness, "colorMode": "color", "speed": 0, "red": Defaults.color.rgb.red, "green": Defaults.color.rgb.green, "blue": Defaults.color.rgb.blue, } } }, 200) details_valceno = ( { "traceId": TRACE_ID, "code": 0, "msg": "request success", "result": { "traceId": TRACE_ID, "code": 0, "result": { "enabled": "on", "colorMode": "color", "brightness": Defaults.brightness, "colorTemp": Defaults.color_temp, "hue": Defaults.color.hsv.hue*27.7778, "saturation": Defaults.color.hsv.saturation*100, "value": Defaults.color.hsv.value, } } }, 200 ) DETAILS_RESPONSES = { 'ESL100': BulbDetails.details_esl100, 'ESL100CW': BulbDetails.details_esl100cw, 'ESL100MC': BulbDetails.details_esl100mc, 'XYD0001': BulbDetails.details_valceno, } def valceno_set_status_response(kwargs=None): default_resp = { "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "enabled": "on", "colorMode": "hsv", "brightness": Defaults.brightness, "colorTemp": Defaults.color_temp, "hue": Defaults.color.hsv.hue*27.7778, "saturation": Defaults.color.hsv.saturation*100, "value": Defaults.color.hsv.value } } } if kwargs is not None and isinstance(kwargs, dict): if kwargs.get('hue') is not None: default_resp['result']['result']['hue'] = kwargs['hue'] * 27.7778 if kwargs.get('saturation') is not None: default_resp['result']['result']['saturation'] = kwargs['saturation'] * 100 if kwargs.get('value') is not None: default_resp['result']['result']['value'] = kwargs['value'] return default_resp, 200 METHOD_RESPONSES = {k: deepcopy(FunctionResponses) for k in BULBS} METHOD_RESPONSES['XYD0001'].default_factory = lambda: ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0 } }, 200) METHOD_RESPONSES['ESL100MC'].default_factory = lambda: ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0 } }, 200) XYD0001_RESP = { 'set_brightness': valceno_set_status_response, 'set_color_temp': valceno_set_status_response, 'set_hsv': valceno_set_status_response, 'set_rgb': valceno_set_status_response, } METHOD_RESPONSES['XYD0001'].update(XYD0001_RESP) pyvesync-2.1.18/src/tests/call_json_fans.py000066400000000000000000000334761475441503300207640ustar00rootroot00000000000000""" Air Purifier and Humidifier Device API Responses FANS variable is a list of device types DETAILS_RESPONSES variable is a dictionary of responses from the API for get_details() methods. The keys are the device types and the values are the responses. The responses are tuples of (response, status) METHOD_RESPONSES variable is a defaultdict of responses from the API. This is the FunctionResponse variable from the utils module in the tests dir. The default response is a tuple with the value ({"code": 0, "msg": "success"}, 200). The values of METHOD_RESPONSES can be a function that takes a single argument or a static value. The value is checked if callable at runtime and if so, it is called with the provided argument. If not callable, the value is returned as is. METHOD_RESPONSES = { 'DEV_TYPE': defaultdict( lambda: ({"code": 0, "msg": "success"}, 200)) ) } # For a function to handle the response def status_response(request_body=None): # do work with request_body return request_body, 200 METHOD_RESPONSES['DEV_TYPE']['set_status'] = status_response # To change the default value for a device type METHOD_RESPONSES['DEVTYPE'].default_factory = lambda: ({"code": 0, "msg": "success"}, 200) """ from copy import deepcopy from pyvesync import vesyncfan, helpers from utils import Defaults, FunctionResponses HUMID_MODELS = [] for model_type, model_dict in vesyncfan.humid_features.items(): HUMID_MODELS.append(model_dict["models"][0]) AIR_MODELS = [] for model_type, model_dict in vesyncfan.air_features.items(): AIR_MODELS.append(model_dict["models"][0]) FANS = HUMID_MODELS + AIR_MODELS FANS_NUM = len(FANS) # FANS = ['Core200S', 'Core300S', 'Core400S', 'Core600S', 'LV-PUR131S', 'LV600S', # 'Classic300S', 'Classic200S', 'Dual200S', 'LV600S'] def INNER_RESULT(inner: dict) -> dict: return { "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "module": None, "stacktrace": None, "result": {"traceId": Defaults.trace_id, "code": 0, "result": inner}, } class FanDefaults: fan_level = 1 filter_life = 80 humidity = 50 mist_level = 3 warm_mist_level = 2 air_quality = 3 air_quality_value = 4 filter_open = 0 aq_percent = 75 pm1 = 10 pm10 = 5 rotate_angle = 45 class FanDetails: details_air = ({ "code": 0, "msg": None, "deviceStatus": "on", "connectionStatus": "online", "activeTime": Defaults.active_time, "deviceImg": None, "deviceName": "LV-PUR131S-NAME", "filterLife": { "change": False, "useHour": None, "percent": FanDefaults.filter_life, }, "airQuality": "excellent", "screenStatus": "on", "mode": "manual", "level": FanDefaults.fan_level, "schedule": None, "timer": None, "scheduleCount": 0, }, 200) details_lv600s = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "请求成功", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "enabled": True, "humidity": FanDefaults.humidity, "mist_virtual_level": 8, "mist_level": FanDefaults.mist_level, "mode": "manual", "water_lacks": False, "humidity_high": False, "water_tank_lifted": False, "display": False, "automatic_stop_reach_target": True, "night_light_brightness": 0, "warm_mist_level": 0, "warm_mist_enabled": False, "configuration": { "auto_target_humidity": 50, "display": False, "automatic_stop": True, }, }, }, }, 200) details_classic200s300s = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "请求成功", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "enabled": True, "humidity": FanDefaults.humidity, "mist_virtual_level": 8, "mist_level": FanDefaults.mist_level, "mode": "manual", "water_lacks": False, "humidity_high": False, "water_tank_lifted": False, "display": False, "automatic_stop_reach_target": True, "night_light_brightness": 0, "configuration": { "auto_target_humidity": 50, "display": False, "automatic_stop": True, }, }, }, }, 200) details_oasismist1000S = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "powerSwitch": 0, "humidity": FanDefaults.humidity, "targetHumidity": 50, "virtualLevel": 1, "mistLevel": FanDefaults.mist_level, "workMode": "manual", "waterLacksState": 0, "waterTankLifted": 0, "autoStopSwitch": 1, "autoStopState": 0, "screenSwitch": 1, "screenState": 0, "scheduleCount": 0, "timerRemain": 0, "errorCode": 0, }, }, }, 200) details_core = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "enabled": True, "filter_life": FanDefaults.filter_life, "mode": "manual", "level": FanDefaults.fan_level, "air_quality": FanDefaults.air_quality, "air_quality_value": FanDefaults.air_quality_value, "display": True, "child_lock": True, "configuration": { "display": True, "display_forever": True, "auto_preference": {"type": "default", "room_size": 0}, }, "extension": {"schedule_count": 0, "timer_remain": 0}, "device_error_code": 0, }, }, }, 200) details_vital100s = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "module": None, "stacktrace": None, "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "powerSwitch": Defaults.bin_toggle, "filterLifePercent": FanDefaults.filter_life, "workMode": "manual", "manualSpeedLevel": FanDefaults.fan_level, "fanSpeedLevel": FanDefaults.fan_level, "AQLevel": FanDefaults.air_quality, "PM25": FanDefaults.air_quality_value, "screenState": Defaults.bin_toggle, "childLockSwitch": Defaults.bin_toggle, "screenSwitch": Defaults.bin_toggle, "lightDetectionSwitch": Defaults.bin_toggle, "environmentLightState": Defaults.bin_toggle, "autoPreference": {"autoPreferenceType": "default", "roomSize": 0}, "scheduleCount": 0, "timerRemain": 0, "efficientModeTimeRemain": 0, "sleepPreference": { "sleepPreferenceType": "default", "cleaningBeforeBedSwitch": 1, "cleaningBeforeBedSpeedLevel": 3, "cleaningBeforeBedMinutes": 5, "whiteNoiseSleepAidSwitch": 1, "whiteNoiseSleepAidSpeedLevel": 1, "whiteNoiseSleepAidMinutes": 45, "duringSleepSpeedLevel": 5, "duringSleepMinutes": 480, "afterWakeUpPowerSwitch": 1, "afterWakeUpWorkMode": "auto", "afterWakeUpFanSpeedLevel": 1, }, "errorCode": 0, }, }, }, 200) details_superior6000S = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "module": None, "stacktrace": None, "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "powerSwitch": 1, "humidity": 44, "targetHumidity": 50, "virtualLevel": 1, "mistLevel": 1, "workMode": "manual", "waterLacksState": 0, "waterTankLifted": 0, "autoStopSwitch": 1, "autoStopState": 0, "screenSwitch": 1, "screenState": 1, "scheduleCount": 0, "timerRemain": 0, "errorCode": 0, "dryingMode": { "dryingLevel": 1, "autoDryingSwitch": 1, "dryingState": 2, "dryingRemain": 7200, }, "autoPreference": 1, "childLockSwitch": 0, "filterLifePercent": 93, "temperature": 662, }, }, }, 200) details_everest = ({ "traceId": "1691789977402", "code": 0, "msg": "request success", "module": None, "stacktrace": None, "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "fanRotateAngle": FanDefaults.rotate_angle, "filterOpenState": FanDefaults.filter_open, "powerSwitch": Defaults.bin_toggle, "filterLifePercent": FanDefaults.filter_life, "workMode": "auto", "manualSpeedLevel": FanDefaults.fan_level, "fanSpeedLevel": FanDefaults.fan_level, "AQLevel": FanDefaults.air_quality, "AQPercent": FanDefaults.aq_percent, "PM25": FanDefaults.air_quality_value, "PM1": FanDefaults.pm1, "PM10": FanDefaults.pm10, "screenState": Defaults.bin_toggle, "childLockSwitch": Defaults.bin_toggle, "screenSwitch": Defaults.bin_toggle, "lightDetectionSwitch": Defaults.bin_toggle, "environmentLightState": Defaults.bin_toggle, "autoPreference": {"autoPreferenceType": "default", "roomSize": 0}, "routine": {"routineType": "normal", "runSeconds": 0}, "scheduleCount": 0, "timerRemain": 0, "efficientModeTimeRemain": 0, "ecoModeRunTime": 0, "errorCode": 0, }, }, }, 200) details_towerfan42 = ({ "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "module": None, "stacktrace": None, "result": { "traceId": Defaults.trace_id, "code": 0, "result": { "powerSwitch": Defaults.bin_toggle, "workMode": "turbo", "manualSpeedLevel": 3, "fanSpeedLevel": 12, "screenState": Defaults.bin_toggle, "screenSwitch": Defaults.bin_toggle, "oscillationSwitch": Defaults.bin_toggle, "oscillationState": Defaults.bin_toggle, "muteSwitch": Defaults.bin_toggle, "muteState": Defaults.bin_toggle, "timerRemain": 0, "temperature": 750, "sleepPreference": { "sleepPreferenceType": "default", "oscillationSwitch": Defaults.bin_toggle, "initFanSpeedLevel": 0, "fallAsleepRemain": 0, "autoChangeFanLevelSwitch": Defaults.bin_toggle }, "scheduleCount": 0, "displayingType": 0, "errorCode": 0, } }, }, 200) DETAILS_RESPONSES = { "LV-PUR131S": FanDetails.details_air, "Classic300S": FanDetails.details_classic200s300s, "Classic200S": FanDetails.details_classic200s300s, "Dual200S": FanDetails.details_classic200s300s, "LUH-A602S-WUSR": FanDetails.details_lv600s, "Core200S": FanDetails.details_core, "Core300S": FanDetails.details_core, "Core400S": FanDetails.details_core, "Core600S": FanDetails.details_core, "LUH-O451S-WEU": FanDetails.details_lv600s, "LUH-O451S-WUS": FanDetails.details_lv600s, "LAP-V201S-AASR": FanDetails.details_vital100s, "LAP-V102S-AASR": FanDetails.details_vital100s, "LUH-M101S-WUS": FanDetails.details_oasismist1000S, "LEH-S601S-WUS": FanDetails.details_superior6000S, "LAP-EL551S-AUS": FanDetails.details_everest, "LTF-F422S-KEU": FanDetails.details_towerfan42, } FunctionResponses.default_factory = lambda: ( { "traceId": Defaults.trace_id, "code": 0, "msg": "request success", "result": {"traceId": Defaults.trace_id, "code": 0}, }, 200, ) METHOD_RESPONSES = {k: deepcopy(FunctionResponses) for k in FANS} # Add responses for methods with different responses than the default # Timer Responses for k in AIR_MODELS: METHOD_RESPONSES[k]["set_timer"] = (INNER_RESULT({"id": 1}), 200) METHOD_RESPONSES[k]["get_timer"] = ( INNER_RESULT({"id": 1, "remain": 100, "total": 100, "action": "off"}), 200, ) FAN_TIMER = helpers.Timer(100, "off") pyvesync-2.1.18/src/tests/call_json_outlets.py000066400000000000000000000124701475441503300215230ustar00rootroot00000000000000""" Outlet Device API Responses OUTLET variable is a list of device types DETAILS_RESPONSES variable is a dictionary of responses from the API for get_details() methods. The keys are the device types and the values are the responses. The responses are tuples of (response, status) METHOD_RESPONSES variable is a defaultdict of responses from the API. This is the FunctionResponse variable from the utils module in the tests dir. The default response is a tuple with the value ({"code": 0, "msg": "success"}, 200). The values of METHOD_RESPONSES can be a function that takes a single argument or a static value. The value is checked if callable at runtime and if so, it is called with the provided argument. If not callable, the value is returned as is. METHOD_RESPONSES = { 'DEV_TYPE': defaultdict( lambda: ({"code": 0, "msg": "success"}, 200)) ) } ### For a function to handle the response def status_response(request_kwargs=None): # do work with request_kwargs return request_body, 200 METHOD_RESPONSES['DEV_TYPE']['set_status'] = status_response ### To change the default value for a device type METHOD_RESPONSES['DEVTYPE'].default_factory = lambda: ({"code": 0, "msg": "success"}, 200) If changing default response for all devices, change the default factory of the import default dict but make sure to use `deepcopy` to avoid unintended side effects. """ from copy import deepcopy from collections import defaultdict from utils import FunctionResponses, Defaults from pyvesync.vesyncoutlet import outlet_modules OUTLETS = outlet_modules.keys() OUTLETS_NUM = len(OUTLETS) # OUTLETS = ['wifi-switch-1.3', 'ESW03-USA', 'ESW01-EU', 'ESW15-USA', 'ESO15-TB'] class OutletDefaults: voltage = 1 # volts energy = 1 # kilowatt power = 1 # kilowatt-hours round_7a_voltage = '1000:1000' # 1 volt round_7a_power = '1000:1000' # 1 watt class OutletDetails: details_15a = ( { 'code': 0, 'msg': None, 'deviceStatus': 'on', 'connectionStatus': 'online', 'activeTime': Defaults.active_time, 'energy': 1, 'nightLightStatus': 'on', 'nightLightBrightness': 50, 'nightLightAutomode': 'manual', 'power': '1', 'voltage': '1', }, 200, ) details_7a = ( { 'deviceStatus': 'on', 'deviceImg': '', 'activeTime': Defaults.active_time, 'energy': OutletDefaults.energy, 'power': OutletDefaults.round_7a_power, 'voltage': OutletDefaults.round_7a_voltage, }, 200, ) details_10a = ( { 'code': 0, 'msg': None, 'deviceStatus': 'on', 'connectionStatus': 'online', 'activeTime': Defaults.active_time, 'energy': OutletDefaults.energy, 'nightLightStatus': None, 'nightLightBrightness': None, 'nightLightAutomode': None, 'power': OutletDefaults.power, 'voltage': OutletDefaults.voltage, }, 200, ) details_outdoor = ( { 'code': 0, 'msg': None, 'connectionStatus': 'online', 'activeTime': Defaults.active_time, 'energy': OutletDefaults.energy, 'power': OutletDefaults.power, 'voltage': OutletDefaults.voltage, 'deviceStatus': 'on', 'deviceName': Defaults.name('ESO15-TB'), 'subDevices': [ { 'subDeviceNo': 1, 'defaultName': 'Socket A', 'subDeviceName': Defaults.name('ESO15-TB'), 'subDeviceStatus': 'on', }, { 'subDeviceNo': 2, 'defaultName': 'Socket B', 'subDeviceName': Defaults.name('ESO15-TB'), 'subDeviceStatus': 'on', }, ], }, 200, ) bsdgo1_details = ({ "code": 0, "msg": "request success", "result": { "powerSwitch_1": 1, "traceId": "1735308365651", "active_time": Defaults.active_time, "connectionStatus": "online", "code": 0 } }, 200) DETAILS_RESPONSES = { 'wifi-switch-1.3': OutletDetails.details_7a, 'ESW03-USA': OutletDetails.details_10a, 'ESW01-EU': OutletDetails.details_10a, 'ESW15-USA': OutletDetails.details_15a, 'ESO15-TB': OutletDetails.details_outdoor, 'BSDOG01': OutletDetails.bsdgo1_details } ENERGY_HISTORY = ( { 'code': 0, 'energyConsumptionOfToday': 1, 'costPerKWH': 1, 'maxEnergy': 1, 'totalEnergy': 1, 'data': [ 1, 1, ], }, 200, ) METHOD_RESPONSES = {k: deepcopy(FunctionResponses) for k in OUTLETS} for k in METHOD_RESPONSES: METHOD_RESPONSES[k]['get_weekly_energy'] = ENERGY_HISTORY METHOD_RESPONSES[k]['get_monthly_energy'] = ENERGY_HISTORY METHOD_RESPONSES[k]['get_yearly_energy'] = ENERGY_HISTORY # Add BSDGO1 specific responses METHOD_RESPONSES['BSDOG01'] = defaultdict(lambda: ({ "code": 0, "msg": "request success", "result": { "traceId": Defaults.trace_id, "code": 0 } }, 200)) pyvesync-2.1.18/src/tests/call_json_switches.py000066400000000000000000000042341475441503300216540ustar00rootroot00000000000000""" Switch Device API Responses SWITCHES variable is a list of device types DETAILS_RESPONSES variable is a dictionary of responses from the API for get_details() methods. The keys are the device types and the values are the responses. The responses are tuples of (response, status) METHOD_RESPONSES variable is a defaultdict of responses from the API. This is the FunctionResponse variable from the utils module in the tests dir. The default response is a tuple with the value ({"code": 0, "msg": "success"}, 200). """ from copy import deepcopy from pyvesync import vesyncswitch from utils import FunctionResponses, Defaults SWITCHES = vesyncswitch.switch_modules.keys() SWITCHES_NUM = len(SWITCHES) class SwitchDetails: details_ws = ( { 'code': 0, 'msg': None, 'deviceStatus': 'on', 'connectionStatus': 'online', 'activeTime': Defaults.active_time, 'power': 'None', 'voltage': 'None', }, 200, ) details_eswd16 = ({ "code": 0, "msg": "请求成功", "traceId": Defaults.trace_id, "indicatorlightStatus": "on", "timer": None, "schedule": None, "brightness": "100", "startMode": None, "activeTime": Defaults.active_time, "rgbStatus": "on", "rgbValue": { "red": Defaults.color.rgb.red, "blue": Defaults.color.rgb.blue, "green": Defaults.color.rgb.green }, "connectionStatus": "online", "devicename": Defaults.name('ESWD16'), "deviceStatus": "on" }, 200) DETAILS_RESPONSES = { 'ESWL01': SwitchDetails.details_ws, 'ESWD16': SwitchDetails.details_eswd16, 'ESWL03': SwitchDetails.details_ws, } METHOD_RESPONSES = {k: deepcopy(FunctionResponses) for k in SWITCHES} pyvesync-2.1.18/src/tests/conftest.py000066400000000000000000000046611475441503300176300ustar00rootroot00000000000000def pytest_addoption(parser): """Prevent new API's from being written during pipeline tests.""" parser.addoption( "--write_api", action="store_true", default=False, help="run tests without writing API to yaml" ) parser.addoption( "--overwrite", action="store_true", default=False, help="overwrite existing API in yaml - WARNING do not use unless absolutely necessary" ) def pytest_generate_tests(metafunc): if metafunc.cls is None or 'test_x' in metafunc.module.__name__: return if metafunc.config.getoption('--write_api'): write_api = True else: write_api = False if metafunc.config.getoption('--overwrite'): overwrite = True else: overwrite = False metafunc.cls.overwrite = overwrite metafunc.cls.write_api = write_api if 'device' in metafunc.cls.__dict__: device = metafunc.cls.__dict__['device'] if device not in metafunc.cls.__dict__: return devices = metafunc.cls.__dict__[device] if metafunc.function.__name__ == 'test_details': return details_generator(metafunc, device, devices) elif metafunc.function.__name__ == 'test_methods': return method_generator(metafunc, device, devices) def details_generator(metafunc, gen_type, devices): """Parametrize device tests for get_details().""" id_list = [] argvalues = [] for dev_type in devices: id_list.append(f"{gen_type}.{dev_type}.update") argvalues.append([dev_type, 'update']) metafunc.parametrize("dev_type, method", argvalues, ids=id_list) return def method_generator(metafunc, gen_type, devices): """Parametrize device tests for methods.""" if 'base_methods' in metafunc.cls.__dict__: base_methods = metafunc.cls.__dict__['base_methods'] else: base_methods = [] call_dict = {dt: base_methods.copy() for dt in devices} if 'device_methods' in metafunc.cls.__dict__: dev_methods = metafunc.cls.__dict__['device_methods'] for dev in call_dict: if dev in dev_methods: call_dict[dev].extend(dev_methods[dev]) id_list = [] argvalues = [] for dev, methods in call_dict.items(): for method in methods: id_list.append(f'{gen_type}.{dev}.{method}') argvalues.append([dev, method]) metafunc.parametrize("dev_type, method", argvalues, ids=id_list) returnpyvesync-2.1.18/src/tests/request_api.yaml000066400000000000000000000000001475441503300206150ustar00rootroot00000000000000pyvesync-2.1.18/src/tests/test_all_devices.py000066400000000000000000000066271475441503300213200ustar00rootroot00000000000000""" This tests all requests made by the pyvesync library with pytest. All tests inherit from the TestBase class which contains the fixtures and methods needed to run the tests. The `helpers.call_api` method is patched to return a mock response. The method, endpoint, headers and json arguments are recorded in YAML files in the api directory, catagorized in folders by module and files by the class name. The default is to record requests that do not exist and compare requests that already exist. If the API changes, set the overwrite argument to True in order to overwrite the existing YAML file with the new request. """ import logging import call_json import call_json_outlets import call_json_bulbs import call_json_fans import call_json_switches from utils import TestBase, assert_test, parse_args logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) LOGIN_RESPONSE = call_json.LOGIN_RET_BODY def test_device_tests(): """Test to ensure all devices have are defined for testing. All devices should have an entry in the DETAILS_RESPONSES dict with response for get_details() method. This test ensures that all devices have been configured for testing. The details response should be located in `{device_type}Details` class of the respective call_json_{device_type} module and the DETAILS_RESPONSE module variable. The class variable with the details response does not matter, the dictionary key of DETAILS_RESPONSES should match the device type. Examples --------- class FanDetails: "Core200SResponse": {'speed': 1, 'device_status': 'on'} DETAILS_RESPONSES = { 'Core200S': FanDetails.Core200SResponse } Asserts ------- Number of devices for each type has a response defined in the respective `call_json` module. See Also -------- src/tests/README.md - README located in the tests directory """ assert call_json_fans.FANS_NUM == len(call_json_fans.DETAILS_RESPONSES) assert call_json_bulbs.BULBS_NUM == len(call_json_bulbs.DETAILS_RESPONSES) assert call_json_outlets.OUTLETS_NUM == len(call_json_outlets.DETAILS_RESPONSES) assert call_json_switches.SWITCHES_NUM == len(call_json_switches.DETAILS_RESPONSES) class TestGeneralAPI(TestBase): """General API testing class for login() and get_devices().""" def test_login(self): """Test login() method request and API response.""" print("Test Login") self.mock_api.return_value = LOGIN_RESPONSE self.manager.enabled = False assert self.manager.login() all_kwargs = parse_args(self.mock_api) assert assert_test(self.manager.login, all_kwargs, None, self.write_api, self.overwrite) def test_get_devices(self): """Test get_devices() method request and API response.""" print("Test Device List") self.mock_api.return_value = call_json.DeviceList.device_list_response() self.manager.get_devices() all_kwargs = parse_args(self.mock_api) assert assert_test(self.manager.get_devices, all_kwargs, None, self.write_api, self.overwrite) assert len(self.manager.bulbs) == call_json_bulbs.BULBS_NUM assert len(self.manager.outlets) == call_json_outlets.OUTLETS_NUM assert len(self.manager.fans) == call_json_fans.FANS_NUM assert len(self.manager.switches) == call_json_switches.SWITCHES_NUM pyvesync-2.1.18/src/tests/test_bulbs.py000066400000000000000000000231231475441503300201430ustar00rootroot00000000000000""" This tests requests made by bulb devices. All tests inherit from the TestBase class which contains the fixtures and methods needed to run the tests. The tests are automatically parametrized by `pytest_generate_tests` in conftest.py. The two methods that are parametrized are `test_details` and `test_methods`. The class variables are used to build the list of devices, test methods and arguments. The `helpers.call_api` method is patched to return a mock response. The method, endpoint, headers and json arguments are recorded in YAML files in the api directory, catagorized in folders by module and files by the class name. The default is to record requests that do not exist and compare requests that already exist. If the API changes, set the overwrite argument to True in order to overwrite the existing YAML file with the new request. See Also -------- `utils.TestBase` - Base class for all tests, containing mock objects `confest.pytest_generate_tests` - Parametrizes tests based on method names & class attributes `call_json_bulbs` - Contains API responses """ import logging import math from pyvesync.vesync import object_factory from pyvesync.vesyncbulb import pct_to_kelvin from utils import TestBase, assert_test, parse_args, Defaults import call_json import call_json_bulbs logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) DEFAULT_COLOR = Defaults.color DEFAULT_COLOR_RGB = dict(DEFAULT_COLOR.rgb._asdict()) DEFAULT_COLOR_HSV = dict(DEFAULT_COLOR.hsv._asdict()) RGB_SET = { 'red': 50, 'green': 200, 'blue': 255, } HSV_SET = { 'hue': 200, 'saturation': 50, 'value': 100, } class TestBulbs(TestBase): """Bulbs testing class. This class tests bulb device details and methods. The methods are parametrized from the class variables using `pytest_generate_tests`. The call_json_bulbs module contains the responses for the API requests. The device is instantiated from the details provided by `call_json.DeviceList.device_list_item()`. Inherits from `utils.TestBase`. Instance Attributes ------------------- self.manager : VeSync Instantiated VeSync object self.mock_api : Mock Mock with patched `helpers.call_api` method self.caplog : LogCaptureFixture Pytest fixture for capturing logs Class Attributes ----------------- device : str Name of device type - bulbs bulbs : list List of device types for bulbs, this variable is named after the device class attribute value base_methods : List[List[str, Dict[str, Any]]] List of common methods for all devices device_methods : Dict[List[List[str, Dict[str, Any]]]] Dictionary of methods specific to device types Methods -------- test_details() Test the device details API request and response test_methods() Test device methods API request and response Examples -------- >>> device = 'bulbs' >>> bulbs = call_json_bulbs.bulbs >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'ESWD16': [['method1'], ['method2', {'kwargs': 'value'}]] } """ device = 'bulbs' bulbs = call_json_bulbs.BULBS base_methods = [['turn_on'], ['turn_off'], ['set_brightness', {'brightness': 50}]] device_methods = { 'ESL100CW': [['set_color_temp', {'color_temp': 50}]], 'ESL100MC': [['set_rgb', RGB_SET], ['enable_white_mode']], 'XYD0001': [['set_hsv', HSV_SET], ['set_color_temp', {'color_temp': 50}], ['enable_white_mode'] ] } def test_details(self, dev_type, method): """Test the device details API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class attribute `device` (name of device type - bulbs), device name (bulbs) list of device types. Example: >>> device = 'bulbs' >>> bulbs = call_json_bulbs.BULBS See Also -------- `utils.TestBase` class docstring `call_json_bulbs` module docstring Notes ------ The device is instantiated using the `call_json.DeviceList.device_list_item()` method. The device details contain the default values set in `utils.Defaults` """ # Set return value for call_api based on call_json_bulb.DETAILS_RESPONSES return_val = call_json_bulbs.DETAILS_RESPONSES[dev_type] self.mock_api.return_value = return_val # Instantiate device from device list return item device_config = call_json.DeviceList.device_list_item(dev_type) _, bulb_obj = object_factory(dev_type, device_config, self.manager) method_call = getattr(bulb_obj, method) method_call() # Parse mock_api args tuple from arg, kwargs to kwargs all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) # Assert device details match expected values assert bulb_obj.brightness == Defaults.brightness if bulb_obj.rgb_shift_feature: assert self._assert_color(bulb_obj) if bulb_obj.color_temp_feature: assert bulb_obj.color_temp_pct == Defaults.color_temp assert bulb_obj.color_temp_kelvin == pct_to_kelvin(Defaults.color_temp) def test_methods(self, dev_type, method): """Test device methods API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - bulbs), device name (bulbs) list of device types, `base_methods` - list of methods for all devices, and `device_methods` - list of methods for each device type. Example: >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } Notes ----- The response can be a callable that accepts the `kwargs` argument to sync the device response with the API response. In some cases the API returns data from the method call, such as `get_yearly_energy`, in other cases the API returns a simple confirmation the command was successful. See Also -------- `TestBase` class method `call_json_bulbs` module """ # Get method name and kwargs from method fixture method_name = method[0] if len(method) == 2 and isinstance(method[1], dict): method_kwargs = method[1] else: method_kwargs = {} # Set return value for call_api based on call_json_bulbs.METHOD_RESPONSES method_response = call_json_bulbs.METHOD_RESPONSES[dev_type][method_name] if callable(method_response): if method_kwargs: self.mock_api.return_value = method_response(method_kwargs) else: self.mock_api.return_value = method_response() else: self.mock_api.return_value = method_response # Get device configuration from call_json.DeviceList.device_list_item() device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device from device list return item _, bulb_obj = object_factory(dev_type, device_config, self.manager) # Get method from device object method_call = getattr(bulb_obj, method[0]) # Ensure method runs based on device configuration if method[0] == 'turn_on': bulb_obj.device_status = 'off' elif method[0] == 'turn_off': bulb_obj.device_status = 'on' # Call method with kwargs if defined if method_kwargs: method_call(**method_kwargs) else: method_call() # Parse arguments from mock_api call into a dictionary all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) def _assert_color(self, bulb_obj): assert math.isclose(bulb_obj.color_rgb.red, DEFAULT_COLOR.rgb.red, rel_tol=1) assert math.isclose(bulb_obj.color.rgb.red, DEFAULT_COLOR.rgb.red, rel_tol=1) assert math.isclose(bulb_obj.color_rgb.green, DEFAULT_COLOR.rgb.green, rel_tol=1) assert math.isclose(bulb_obj.color.rgb.green, DEFAULT_COLOR.rgb.green, rel_tol=1) assert math.isclose(bulb_obj.color_rgb.blue, DEFAULT_COLOR.rgb.blue, rel_tol=1) assert math.isclose(bulb_obj.color.rgb.blue, DEFAULT_COLOR.rgb.blue, rel_tol=1) assert math.isclose(bulb_obj.color_hsv.hue, DEFAULT_COLOR.hsv.hue, rel_tol=1) assert math.isclose(bulb_obj.color_hue, DEFAULT_COLOR.hsv.hue, rel_tol=1) assert math.isclose(bulb_obj.color_hsv.saturation, DEFAULT_COLOR.hsv.saturation, rel_tol=1) assert math.isclose(bulb_obj.color_saturation, DEFAULT_COLOR.hsv.saturation, rel_tol=1) assert math.isclose(bulb_obj.color_hsv.value, DEFAULT_COLOR.hsv.value, rel_tol=1) assert math.isclose(bulb_obj.color_value, DEFAULT_COLOR.hsv.value, rel_tol=1) return True pyvesync-2.1.18/src/tests/test_fans.py000066400000000000000000000357361475441503300200000ustar00rootroot00000000000000""" This tests requests for Humidifiers and Air Purifiers. All tests inherit from the TestBase class which contains the fixtures and methods needed to run the tests. The tests are automatically parametrized by `pytest_generate_tests` in conftest.py. The two methods that are parametrized are `test_details` and `test_methods`. The class variables are used to build the list of devices, test methods and arguments. The `helpers.call_api` method is patched to return a mock response. The method, endpoint, headers and json arguments are recorded in YAML files in the api directory, catagorized in folders by module and files by the class name. The default is to record requests that do not exist and compare requests that already exist. If the API changes, set the overwrite argument to True in order to overwrite the existing YAML file with the new request. See Also -------- `utils.TestBase` - Base class for all tests, containing mock objects `confest.pytest_generate_tests` - Parametrizes tests based on method names & class attributes `call_json_fans` - Contains API responses """ import logging from pyvesync.vesync import object_factory from utils import TestBase, assert_test, parse_args, Defaults import call_json import call_json_fans logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) DEFAULT_COLOR = Defaults.color DEFAULT_COLOR_RGB = dict(DEFAULT_COLOR.rgb._asdict()) DEFAULT_COLOR_HSV = dict(DEFAULT_COLOR.hsv._asdict()) RGB_SET = { 'red': 50, 'green': 200, 'blue': 255, } HSV_SET = { 'hue': 200, 'saturation': 50, 'value': 100, } class TestAirPurifiers(TestBase): """Air Purifier testing class. This class tests Air Purifier device details and methods. The methods are parametrized from the class variables using `pytest_generate_tests`. The call_json_fans module contains the responses for the API requests. The device is instantiated from the details provided by `call_json.DeviceList.device_list_item()`. Inherits from `utils.TestBase`. Instance Attributes ------------------- self.manager : VeSync Instantiated VeSync object self.mock_api : Mock Mock with patched `helpers.call_api` method self.caplog : LogCaptureFixture Pytest fixture for capturing logs Class Attributes --------------- device : str Name of device type - air_purifiers air_purifiers : list List of device types for air purifiers, this variable is named after the device variable value base_methods : List[List[str, Dict[str, Any]]] List of common methods for all devices device_methods : Dict[List[List[str, Dict[str, Any]]]] Dictionary of methods specific to device types Methods -------- test_details() Test the device details API request and response test_methods() Test device methods API request and response Examples -------- >>> device = 'air_purifiers' >>> air_purifiers = call_json_fans.AIR_MODELS >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'ESWD16': [['method1'], ['method2', {'kwargs': 'value'}]] } """ device = 'air_purifiers' air_purifiers = call_json_fans.AIR_MODELS base_methods = [['turn_on'], ['turn_off'], ['sleep_mode'], ['manual_mode'], ['change_fan_speed', {'speed': 3}]] device_methods = { 'Core300S': [['auto_mode'], ['turn_on_display'], ['turn_off_display'], ['set_timer', {'timer_duration': 100}], ['clear_timer']], 'Core400S': [['auto_mode'], ['turn_on_display'], ['turn_off_display'], ['set_timer', {'timer_duration': 100}], ['clear_timer']], 'Core600S': [['auto_mode'], ['turn_on_display'], ['turn_off_display'], ['set_timer', {'timer_duration': 100}], ['clear_timer']], } def test_details(self, dev_type, method): """Test the device details API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - air_purifiers), device name (air_purifiers) list of device types. Example: >>> device = 'air_purifiers' >>> air_purifiers = call_json_fans.AIR_MODELS See Also -------- `utils.TestBase` class docstring `call_json_fans` module docstring Notes ------ The device is instantiated using the `call_json.DeviceList.device_list_item()` method. The device details contain the default values set in `utils.Defaults` """ # Set return value for call_api based on call_json_fan.DETAILS_RESPONSES return_val = call_json_fans.DETAILS_RESPONSES[dev_type] self.mock_api.return_value = return_val # Instantiate device from device list return item device_config = call_json.DeviceList.device_list_item(dev_type) _, fan_obj = object_factory(dev_type, device_config, self.manager) method_call = getattr(fan_obj, method) method_call() # Parse mock_api args tuple from arg, kwargs to kwargs all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) def test_methods(self, dev_type, method): """Test device methods API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - air_purifiers), device name (air_purifiers) list of device types, `base_methods` - list of methods for all devices, and `device_methods` - list of methods for each device type. Example: >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } Notes ----- The response can be a callable that accepts the `kwargs` argument to sync the device response with the API response. In some cases the API returns data from the method call, such as `get_yearly_energy`, in other cases the API returns a simple confirmation the command was successful. See Also -------- `TestBase` class method `call_json_fans` module """ # Get method name and kwargs from method fixture method_name = method[0] if len(method) == 2 and isinstance(method[1], dict): method_kwargs = method[1] else: method_kwargs = {} # Set return value for call_api based on call_json_fans.METHOD_RESPONSES method_response = call_json_fans.METHOD_RESPONSES[dev_type][method_name] if callable(method_response): if method_kwargs: self.mock_api.return_value = method_response(method_kwargs) else: self.mock_api.return_value = method_response() else: self.mock_api.return_value = method_response # Get device configuration from call_json.DeviceList.device_list_item() device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device from device list return item _, fan_obj = object_factory(dev_type, device_config, self.manager) # Get method from device object method_call = getattr(fan_obj, method[0]) # Ensure method runs based on device configuration if method[0] == 'turn_on': fan_obj.device_status = 'off' elif method[0] == 'turn_off': fan_obj.device_status = 'on' elif method[0] == 'change_fan_speed': fan_obj.mode = 'manual' fan_obj.details['level'] = 1 elif method[0] == 'clear_timer': fan_obj.timer = call_json_fans.FAN_TIMER # Call method with kwargs if defined if method_kwargs: method_call(**method_kwargs) else: method_call() # Parse arguments from mock_api call into a dictionary all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) class TestHumidifiers(TestBase): """Humidifier testing class. This class tests Humidifier device details and methods. The methods are parametrized from the class variables using `pytest_generate_tests`. The call_json_fans module contains the responses for the API requests. The device is instantiated from the details provided by `call_json.DeviceList.device_list_item()`. Inherits from `utils.TestBase`. Instance Attributes ------------------- self.manager : VeSync Instantiated VeSync object self.mock_api : Mock Mock with patched `helpers.call_api` method self.caplog : LogCaptureFixture Pytest fixture for capturing logs Class Variables --------------- device : str Name of device type - humidifiers humidifers : list List of device types for humidifiers, this variable is named after the device variable value base_methods : List[List[str, Dict[str, Any]]] List of common methods for all devices device_methods : Dict[List[List[str, Dict[str, Any]]]] Dictionary of methods specific to device types Methods -------- test_details() Test the device details API request and response test_methods() Test device methods API request and response Examples -------- >>> device = 'humidifiers' >>> humidifiers = call_json_fans.HUMID_MODELS >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'ESWD16': [['method1'], ['method2', {'kwargs': 'value'}]] } """ device = 'humidifiers' humidifiers = call_json_fans.HUMID_MODELS base_methods = [['turn_on'], ['turn_off'], ['turn_on_display'], ['turn_off_display'], ['automatic_stop_on'], ['automatic_stop_off'], ['set_humidity', {'humidity': 50}], ['set_auto_mode'], ['set_manual_mode'], ['set_mist_level', {'level': 2}] ] device_methods = { 'LUH-A602S-WUSR': [['set_warm_level', {'warm_level': 3}]], 'LEH-S601S-WUS': [['set_drying_mode_enabled', { 'mode': False }]] } def test_details(self, dev_type, method): """Test the device details API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - fans), device name (fans) list of device types. Example: >>> device = 'air_purifiers' >>> air_purifiers = call_json_fans.AIR_MODELS See Also -------- `utils.TestBase` class docstring `call_json_fans` module docstring Notes ------ The device is instantiated using the `call_json.DeviceList.device_list_item()` method. The device details contain the default values set in `utils.Defaults` """ # Set return value for call_api based on call_json_fan.DETAILS_RESPONSES return_val = call_json_fans.DETAILS_RESPONSES[dev_type] self.mock_api.return_value = return_val # Instantiate device from device list return item device_config = call_json.DeviceList.device_list_item(dev_type) _, fan_obj = object_factory(dev_type, device_config, self.manager) method_call = getattr(fan_obj, method) method_call() # Parse mock_api args tuple from arg, kwargs to kwargs all_kwargs = parse_args(self.mock_api) # Assert request matches recorded request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) def test_methods(self, dev_type, method): """Test device methods API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - humidifiers), device name (humidifiers) list of device types, `base_methods` - list of methods for all devices, and `device_methods` - list of methods for each device type. Example: >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } Notes ----- The response can be a callable that accepts the `kwargs` argument to sync the device response with the API response. In some cases the API returns data from the method call, such as `get_yearly_energy`, in other cases the API returns a simple confirmation the command was successful. See Also -------- `TestBase` class method `call_json_fans` module """ # Get method name and kwargs from method fixture method_name = method[0] if len(method) == 2 and isinstance(method[1], dict): method_kwargs = method[1] else: method_kwargs = {} # Set return value for call_api based on call_json_fans.METHOD_RESPONSES method_response = call_json_fans.METHOD_RESPONSES[dev_type][method_name] if callable(method_response): if method_kwargs: self.mock_api.return_value = method_response(method_kwargs) else: self.mock_api.return_value = method_response() else: self.mock_api.return_value = method_response # Get device configuration from call_json.DeviceList.device_list_item() device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device from device list return item _, fan_obj = object_factory(dev_type, device_config, self.manager) # Get method from device object method_call = getattr(fan_obj, method[0]) # Ensure method runs based on device configuration if method[0] == 'turn_on': fan_obj.device_status = 'off' elif method[0] == 'turn_off': fan_obj.device_status = 'on' # Call method with kwargs if defined if method_kwargs: method_call(**method_kwargs) else: method_call() # Parse arguments from mock_api call into a dictionary all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) pyvesync-2.1.18/src/tests/test_outlets.py000066400000000000000000000244221475441503300205360ustar00rootroot00000000000000""" This tests all requests made by outlet devices. All tests inherit from the TestBase class which contains the fixtures and methods needed to run the tests. The tests are automatically parametrized by `pytest_generate_tests` in conftest.py. The two methods that are parametrized are `test_details` and `test_methods`. The class variables are used to build the list of devices, test methods and arguments. The `helpers.call_api` method is patched to return a mock response. The method, endpoint, headers and json arguments are recorded in YAML files in the api directory, catagorized in folders by module and files by the class name. The default is to record requests that do not exist and compare requests that already exist. If the API changes, set the overwrite argument to True in order to overwrite the existing YAML file with the new request. See Also -------- `utils.TestBase` - Base class for all tests, containing mock objects `confest.pytest_generate_tests` - Parametrizes tests based on method names & class attributes `call_json_outlets` - Contains API responses """ import pytest import logging from pyvesync.vesync import object_factory from utils import TestBase, assert_test, parse_args import call_json import call_json_outlets from utils import Defaults logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) OUTLET_DEV_TYPES = call_json_outlets.OUTLETS POWER_METHODS = ['get_energy_update'] OUTLET_PARAMS = [[dev, method] for dev in OUTLET_DEV_TYPES for method in POWER_METHODS] class TestOutlets(TestBase): """Outlets testing class. This class tests outlets device details and methods. The methods are parametrized from the class variables using `pytest_generate_tests`. The call_json_outlets module contains the responses for the API requests. The device is instantiated from the details provided by `call_json.DeviceList.device_list_item()`. Inherits from `utils.TestBase`. Instance Attributes ------------------- self.manager : VeSync Instantiated VeSync object self.mock_api : Mock Mock with patched `helpers.call_api` method self.caplog : LogCaptureFixture Pytest fixture for capturing logs Class Attributes --------------- device : str Name of device type - outlets outlets : list List of device types for outlets, this variable is named after the device variable value base_methods : List[List[str, Dict[str, Any]]] List of common methods for all devices device_methods : Dict[List[List[str, Dict[str, Any]]]] Dictionary of methods specific to device types Methods -------- test_details() Test the device details API request and response test_methods() Test device methods API request and response Examples -------- >>> device = 'outlets' >>> outlets = ['ESW01-USA', 'ESW01-EU', 'ESW01-AU'] >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } """ device = 'outlets' outlets = call_json_outlets.OUTLETS base_methods = [ ['turn_on'], ['turn_off'] ] device_methods = { 'ESW15-USA': [ ['turn_on_nightlight'], ['turn_off_nightlight'] ], 'wifi-switch-1.3': [ ['get_weekly_energy'], ['get_monthly_energy'], ['get_yearly_energy'] ], 'ESW03-USA': [ ['get_weekly_energy'], ['get_monthly_energy'], ['get_yearly_energy'] ], 'ESW01-EU': [ ['get_weekly_energy'], ['get_monthly_energy'], ['get_yearly_energy'] ], 'ESW15-USA': [ ['get_weekly_energy'], ['get_monthly_energy'], ['get_yearly_energy'] ], 'ESO15-TB': [ ['get_weekly_energy'], ['get_monthly_energy'], ['get_yearly_energy'] ] } def test_details(self, dev_type, method): """Test the device details API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - outlets), device name (outlets) list of device types. Example: >>> device = 'outlets' >>> outlets = ['ESW01-USA', 'ESW01-EU', 'ESW01-AU'] See Also -------- `utils.TestBase` class docstring `call_json_outlets` module docstring Notes ------ The device is instantiated using the `call_json.DeviceList.device_list_item()` method. The device details contain the default values set in `utils.Defaults` """ # Get response for device details details_response = call_json_outlets.DETAILS_RESPONSES[dev_type] if callable(details_response): self.mock_api.return_value = details_response() else: self.mock_api.return_value = details_response # Get device configuration device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device _, outlet_obj = object_factory(dev_type, device_config, self.manager) # Call get_details() directly outlet_obj.get_details() # Parse arguments from mock_api call into dictionary all_kwargs = parse_args(self.mock_api) # Set both write_api and overwrite to True to update YAML files assert_test(outlet_obj.get_details, all_kwargs, dev_type, write_api=True, overwrite=True) # Test bad responses self.mock_api.reset_mock() if dev_type == 'wifi-switch-1.3': self.mock_api.return_value = (None, 400) else: self.mock_api.return_value = call_json.DETAILS_BADCODE outlet_obj.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_methods(self, dev_type, method): """Test device methods API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - outlets), device name (outlets) list of device types, `base_methods` - list of methods for all devices, and `device_methods` - list of methods for each device type. Example: >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } Notes ----- The response can be a callable that accepts the `kwargs` argument to sync the device response with the API response. In some cases the API returns data from the method call, such as `get_yearly_energy`, in other cases the API returns a simple confirmation the command was successful. See Also -------- `TestBase` class method `call_json_outlets` module """ # Get method name and kwargs from method fixture method_name = method[0] if len(method) == 2 and isinstance(method[1], dict): method_kwargs = method[1] else: method_kwargs = {} # Set return value for call_api based on METHOD_RESPONSES method_response = call_json_outlets.METHOD_RESPONSES[dev_type][method_name] if callable(method_response): if method_kwargs: self.mock_api.return_value = method_response(**method_kwargs) else: self.mock_api.return_value = method_response() else: self.mock_api.return_value = method_response # Get device configuration device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device _, outlet_obj = object_factory(dev_type, device_config, self.manager) # Get method from device object method_call = getattr(outlet_obj, method[0]) # Ensure method runs based on device configuration if method[0] == 'turn_on': outlet_obj.device_status = 'off' elif method[0] == 'turn_off': outlet_obj.device_status = 'on' # Call method with kwargs if present if method_kwargs: method_call(**method_kwargs) else: method_call() # Parse arguments from mock_api call into dictionary all_kwargs = parse_args(self.mock_api) # Assert request matches recorded request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) # Test bad responses self.mock_api.reset_mock() if dev_type == 'wifi-switch-1.3': self.mock_api.return_value = (None, 400) else: self.mock_api.return_value = call_json.DETAILS_BADCODE if method[0] == 'turn_on': outlet_obj.device_status = 'off' if method[0] == 'turn_off': outlet_obj.device_status = 'on' if 'energy' in method[0]: return assert method_call() is False @pytest.mark.parametrize('dev_type', [d for d in OUTLET_DEV_TYPES if d != 'BSDOG01']) def test_power(self, dev_type): """Test outlets power history methods.""" self.mock_api.return_value = call_json_outlets.ENERGY_HISTORY device_config = call_json.DeviceList.device_list_item(dev_type) _, outlet_obj = object_factory(dev_type, device_config, self.manager) outlet_obj.update_energy() assert self.mock_api.call_count == 3 assert list(outlet_obj.energy.keys()) == ['week', 'month', 'year'] self.mock_api.reset_mock() outlet_obj.energy = {} if dev_type == 'wifi-switch-1.3': self.mock_api.return_value = (None, 400) else: self.mock_api.return_value = call_json.DETAILS_BADCODE outlet_obj.update_energy() self.mock_api.call_count == 0 outlet_obj.update_energy(bypass_check=True) self.mock_api.assert_called_once() assert 'Unable to get' in self.caplog.records[-1].message pyvesync-2.1.18/src/tests/test_switches.py000066400000000000000000000210141475441503300206620ustar00rootroot00000000000000""" This tests requests made by switch devices. All tests inherit from the TestBase class which contains the fixtures and methods needed to run the tests. The tests are automatically parametrized by `pytest_generate_tests` in conftest.py. The two methods that are parametrized are `test_details` and `test_methods`. The class variables are used to build the list of devices, test methods and arguments. The `helpers.call_api` method is patched to return a mock response. The method, endpoint, headers and json arguments are recorded in YAML files in the api directory, catagorized in folders by module and files by the class name. The default is to record requests that do not exist and compare requests that already exist. If the API changes, set the overwrite argument to True in order to overwrite the existing YAML file with the new request. See Also -------- `utils.TestBase` - Base class for all tests, containing mock objects `confest.pytest_generate_tests` - Parametrizes tests based on method names & class attributes `call_json_switches` - Contains API responses """ import logging from pyvesync.vesync import object_factory from utils import TestBase, assert_test, parse_args, Defaults import call_json import call_json_switches logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) DEFAULT_COLOR = Defaults.color.rgb COLOR_DICT = { 'red': DEFAULT_COLOR.red, 'blue': DEFAULT_COLOR.blue, 'green': DEFAULT_COLOR.green, } class TestSwitches(TestBase): """Switches testing class. This class tests switch device details and methods. The methods are parametrized from the class variables using `pytest_generate_tests`. The call_json_switches module contains the responses for the API requests. The device is instantiated from the details provided by `call_json.DeviceList.device_list_item()`. Inherits from `utils.TestBase`. Instance Attributes ------------------- self.manager : VeSync Instantiated VeSync object self.mock_api : Mock Mock with patched `helpers.call_api` method self.caplog : LogCaptureFixture Pytest fixture for capturing logs Class Variables --------------- device : str Name of device type - switches switches : list List of device types for switches, this variable is named after the device variable value base_methods : List[List[str, Dict[str, Any]]] List of common methods for all devices device_methods : Dict[List[List[str, Dict[str, Any]]]] Dictionary of methods specific to device types Methods -------- test_details() Test the device details API request and response test_methods() Test device methods API request and response Examples -------- >>> device = 'switches' >>> switches = call_json_switches.SWITCHES >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'ESWD16': [['method1'], ['method2', {'kwargs': 'value'}]] } """ device = 'switches' switches = call_json_switches.SWITCHES base_methods = [['turn_on'], ['turn_off']] device_methods = { 'ESWD16': [['indicator_light_on'], ['rgb_color_on'], ['rgb_color_set', COLOR_DICT], ['set_brightness', {'brightness': Defaults.brightness}]], } def test_details(self, dev_type, method): """Test the device details API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - switches), device name (switches) list of device types. Example: >>> device = 'switches' >>> switches = call_json_switches.SWITCHES See Also -------- `utils.TestBase` class docstring `call_json_switches` module docstring Notes ------ The device is instantiated using the `call_json.DeviceList.device_list_item()` method. The device details contain the default values set in `utils.Defaults` """ # Set return value for call_api based on call_json_bulb.DETAILS_RESPONSES self.mock_api.return_value = call_json_switches.DETAILS_RESPONSES[dev_type] # Instantiate device from device list return item device_config = call_json.DeviceList.device_list_item(dev_type) _, switch_obj = object_factory(dev_type, device_config, self.manager) method_call = getattr(switch_obj, method) method_call() # Parse mock_api args tuple from arg, kwargs to kwargs all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) # Assert device details match expected values assert switch_obj.active_time == Defaults.active_time if switch_obj.is_dimmable(): assert switch_obj.brightness == str(Defaults.brightness) assert switch_obj.indicator_light_status == 'on' assert switch_obj.rgb_light_status == 'on' assert switch_obj.rgb_light_value == COLOR_DICT self.mock_api.reset_mock() self.mock_api.return_value = call_json.DETAILS_BADCODE method_call() assert 'details' in self.caplog.records[-1].message def test_methods(self, dev_type, method): """Test switch methods API request and response. This method is automatically parametrized by `pytest_generate_tests` based on class variables `device` (name of device type - switches), device name (switches) list of device types, `base_methods` - list of methods for all devices, and `device_methods` - list of methods for each device type. Example: >>> base_methods = [['turn_on'], ['turn_off'], ['update']] >>> device_methods = { 'dev_type': [['method1'], ['method2', {'kwargs': 'value'}]] } Notes ----- The response can be a callable that accepts the `kwargs` argument to sync the device response with the API response. In some cases the API returns data from the method call, such as `get_yearly_energy`, in other cases the API returns a simple confirmation the command was successful. See Also -------- `TestBase` class method `call_json_switches` module """ # Get method name and kwargs from method fixture method_name = method[0] if len(method) == 2 and isinstance(method[1], dict): method_kwargs = method[1] else: method_kwargs = {} # Set return value for call_api based on call_json_switches.METHOD_RESPONSES method_response = call_json_switches.METHOD_RESPONSES[dev_type][method_name] if callable(method_response): if method_kwargs: self.mock_api.return_value = method_response(**method_kwargs) else: self.mock_api.return_value = method_response() else: self.mock_api.return_value = method_response # Get device configuration from call_json.DeviceList.device_list_item() device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device from device list return item _, switch_obj = object_factory(dev_type, device_config, self.manager) # Get method from device object method_call = getattr(switch_obj, method[0]) # Ensure method runs based on device configuration if method[0] == 'turn_on': switch_obj.device_status = 'off' elif method[0] == 'turn_off': switch_obj.device_status = 'on' # Call method with kwargs if defined if method_kwargs: method_call(**method_kwargs) else: method_call() # Parse arguments from mock_api call into a dictionary all_kwargs = parse_args(self.mock_api) # Assert request matches recored request or write new records assert_test(method_call, all_kwargs, dev_type, self.write_api, self.overwrite) self.mock_api.reset_mock() self.mock_api.return_value = call_json.DETAILS_BADCODE if method_kwargs: assert method_call(**method_kwargs) is False else: assert method_call() is False pyvesync-2.1.18/src/tests/test_x_air_pur.py000066400000000000000000000124531475441503300210300ustar00rootroot00000000000000"""Levoit Air Purifier tests.""" from pyvesync.vesyncfan import VeSyncAir131, VeSyncAirBypass from pyvesync.helpers import Helpers as helpers import call_json import call_json_fans from utils import TestBase, Defaults LVPUR131S = 'LV-PUR131S' CORE200S = 'Core200S' DEV_LIST_DETAIL = call_json.DeviceList.device_list_item(LVPUR131S) CORE200S_DETAIL = call_json.DeviceList.device_list_item(CORE200S) CORRECT_LIST = call_json.DeviceList.device_list_response(LVPUR131S) CORRECT_DETAILS = call_json_fans.DETAILS_RESPONSES[LVPUR131S] BAD_LIST = call_json.DETAILS_BADCODE class TestVesyncAirPurifier(TestBase): """Air purifier tests.""" def test_airpur_conf(self): """Tests that 15A Outlet is instantiated properly.""" self.mock_api.return_value = CORRECT_LIST self.manager.get_devices() fans = self.manager.fans assert len(fans) == 1 fan = fans[0] assert isinstance(fan, VeSyncAir131) assert fan.device_name == Defaults.name(LVPUR131S) assert fan.device_type == LVPUR131S assert fan.cid == Defaults.cid(LVPUR131S) assert fan.uuid == Defaults.uuid(LVPUR131S) def test_airpur_details(self): """Test Air Purifier get_details().""" self.mock_api.return_value = CORRECT_DETAILS fan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) fan.get_details() dev_details = fan.details assert fan.device_status == 'on' assert isinstance(dev_details, dict) assert dev_details['active_time'] == 1 assert fan.filter_life == call_json_fans.FanDefaults.filter_life assert dev_details['screen_status'] == Defaults.str_toggle assert fan.mode == 'manual' assert dev_details['level'] == call_json_fans.FanDefaults.fan_level assert fan.fan_level == call_json_fans.FanDefaults.fan_level assert dev_details['air_quality'] == 'excellent' assert fan.air_quality == 'excellent' def test_airpur_details_fail(self): """Test Air Purifier get_details with Code>0.""" self.mock_api.return_value = BAD_LIST fan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) fan.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_airpur_onoff(self): """Test Air Purifier Device On/Off Methods.""" self.mock_api.return_value = ({'code': 0}, 200) fan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) head = helpers.req_headers(self.manager) body = helpers.req_body(self.manager, 'devicestatus') fan.device_status = 'off' body['status'] = 'on' body['uuid'] = fan.uuid on = fan.turn_on() self.mock_api.assert_called_with( '/131airPurifier/v1/device/deviceStatus', 'put', json_object=body, headers=head) call_args = self.mock_api.call_args_list[0][0] assert call_args[0] == '/131airPurifier/v1/device/deviceStatus' assert call_args[1] == 'put' assert on fan.device_status = 'on' off = fan.turn_off() body['status'] = 'off' self.mock_api.assert_called_with( '/131airPurifier/v1/device/deviceStatus', 'put', json_object=body, headers=head) assert off def test_airpur_onoff_fail(self): """Test Air Purifier On/Off Fail with Code>0.""" self.mock_api.return_value = ({'code': 1}, 400) vsfan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) assert not vsfan.turn_on() assert not vsfan.turn_off() def test_airpur_fanspeed(self): """Test changing fan speed of.""" self.mock_api.return_value = ({'code': 0}, 200) fan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) fan.mode = 'manual' fan.details['level'] = 1 b = fan.change_fan_speed() assert fan.fan_level == 2 b = fan.change_fan_speed() assert fan.fan_level == 3 b = fan.change_fan_speed() assert fan.fan_level == 1 assert b b = fan.change_fan_speed(2) assert b assert fan.fan_level == 2 def test_mode_toggle(self): """Test changing modes on air purifier.""" self.mock_api.return_value = ({'code': 0}, 200) fan = VeSyncAir131(DEV_LIST_DETAIL, self.manager) f = fan.auto_mode() assert f assert fan.mode == 'auto' f = fan.manual_mode() assert fan.mode == 'manual' assert f f = fan.sleep_mode() assert fan.mode == 'sleep' assert f def test_airpur_set_timer(self): """Test timer function of Core*00S Purifiers.""" self.mock_api.return_value = (call_json_fans.INNER_RESULT({'id': 1}), 200) fan = VeSyncAirBypass(CORE200S_DETAIL, self.manager) fan.set_timer(100) assert fan.timer is not None assert fan.timer.timer_duration == 100 assert fan.timer.done is False assert fan.timer.action == 'off' assert fan.timer.running is True def test_airpur_clear_timer(self): """Test clear_timer method for Core air purifiers.""" self.mock_api.return_value = call_json_fans.FunctionResponses['Core200S'] fan = VeSyncAirBypass(CORE200S_DETAIL, self.manager) fan.timer = call_json_fans.FAN_TIMER fan.clear_timer() assert fan.timer is None pyvesync-2.1.18/src/tests/test_x_vesync.py000066400000000000000000000136011475441503300206720ustar00rootroot00000000000000"""General VeSync tests.""" import unittest import importlib from unittest import mock from unittest.mock import patch, Mock, MagicMock from pyvesync import VeSync from pyvesync.helpers import Helpers class TestVesync(unittest.TestCase): """Test VeSync object initialization.""" def setUp(self): """Setup VeSync argument cases.""" self.vesync_1 = VeSync('sam@email.com', 'password', 'America/New_York') self.vesync_2 = VeSync('sam@email.com', 'password') self.vesync_3 = VeSync('sam@email.com', 'password', None) self.vesync_4 = VeSync('sam@email.com', 'password') self.vesync_5 = VeSync('', '') self.vesync_6 = VeSync(None, None, None) self.vesync_7 = VeSync(None, 'password') self.vesync_8 = VeSync('sam@email.com', None) self.vesync_9 = VeSync('sam@email.com', 'password', 1) def tearDown(self): """Clean up test.""" pass def test_instance(self): """Test VeSync object is successfully initialized.""" self.assertIsInstance(self.vesync_1, VeSync) def test_imports(self): """Test that __all__ contains only names that are actually exported.""" modules = ['pyvesync.vesyncfan', 'pyvesync.vesyncbulb', 'pyvesync.vesyncoutlet', 'pyvesync.vesyncswitch'] for mod in modules: import_mod = importlib.import_module(mod) missing = set(n for n in import_mod.__all__ if getattr(import_mod, n, None) is None) self.assertFalse( missing, msg="__all__ contains unresolved names: %s" % ( ", ".join(missing),)) def test_username(self): """Test invalid username arguments.""" self.assertEqual(self.vesync_1.username, 'sam@email.com') self.assertEqual(self.vesync_5.username, '') self.assertEqual(self.vesync_6.username, None) self.vesync_1.username = 'tom@email.com' self.assertEqual(self.vesync_1.username, 'tom@email.com') def test_password(self): """Test invalid password arguments.""" self.assertEqual(self.vesync_1.password, 'password') self.assertEqual(self.vesync_5.password, '') self.assertEqual(self.vesync_6.password, None) self.vesync_1.password = 'other' self.assertEqual(self.vesync_1.password, 'other') def test_hash_password(self): """Test password hash method.""" self.assertEqual( Helpers.hash_password(self.vesync_1.password), '5f4dcc3b5aa765d61d8327deb882cf99', ) self.assertEqual( Helpers.hash_password(self.vesync_5.password), 'd41d8cd98f00b204e9800998ecf8427e', ) with self.assertRaises(AttributeError): Helpers.hash_password(self.vesync_6.password) def test_time_zone(self): """Test time zone argument handling.""" self.assertEqual(self.vesync_1.time_zone, 'America/New_York') self.assertEqual(self.vesync_2.time_zone, 'America/New_York') self.assertEqual(self.vesync_3.time_zone, 'America/New_York') self.assertEqual(self.vesync_9.time_zone, 'America/New_York') self.vesync_1.time_zone = 'America/East' self.assertEqual(self.vesync_1.time_zone, 'America/East') def test_login(self): """Test login method.""" mock_vesync = mock.Mock() mock_vesync.login.return_value = True self.assertTrue(mock_vesync.login()) mock_vesync.login.return_value = False self.assertFalse(mock_vesync.login()) with patch('pyvesync.helpers.Helpers.call_api') as mocked_post: d = { 'result': { 'accountID': '12346536', 'userType': '1', 'token': 'somevaluehere', }, 'code': 0, } mocked_post.return_value = (d, 200) data = self.vesync_1.login() body = Helpers.req_body(self.vesync_1, 'login') body['email'] = self.vesync_1.username body['password'] = Helpers.hash_password(self.vesync_1.password) mocked_post.assert_called_with('/cloud/v1/user/login', 'post', json_object=body) self.assertTrue(data) class TestApiFunc: """Test call_api() method.""" @patch('pyvesync.helpers.requests.get', autospec=True) def test_api_get(self, get_mock): """Test get api call.""" get_mock.return_value = Mock(ok=True, status_code=200) get_mock.return_value.json.return_value = {'code': 0} mock_return = Helpers.call_api('/call/location', method='get') assert mock_return == ({'code': 0}, 200) @patch('pyvesync.helpers.requests.post', autospec=True) def test_api_post(self, post_mock): """Test post api call.""" post_mock.return_value = Mock(ok=True, status_code=200) post_mock.return_value.json.return_value = {'code': 0} mock_return = Helpers.call_api('/call/location', method='post') assert mock_return == ({'code': 0}, 200) @patch('pyvesync.helpers.requests.put', autospec=True) def test_api_put(self, put_mock): """Test put api call.""" put_mock.return_value = Mock(ok=True, status_code=200) put_mock.return_value.json.return_value = {'code': 0} mock_return = Helpers.call_api('/call/location', method='put') assert mock_return == ({'code': 0}, 200) @patch('pyvesync.helpers.requests.get', autospec=True) def test_api_bad_response(self, api_mock): """Test bad API response handling.""" api_mock.side_effect = MagicMock(status_code=400) mock_return = Helpers.call_api('/test/bad-response', method='get') print(api_mock.call_args_list) assert mock_return == (None, None) if __name__ == '__main__': unittest.main() pyvesync-2.1.18/src/tests/test_x_vesync_10a.py000066400000000000000000000154041475441503300213360ustar00rootroot00000000000000"""Test scripts for Etekcity 10A Outlets.""" import pytest from unittest.mock import patch import logging from pyvesync import VeSync from pyvesync.vesyncoutlet import VeSyncOutlet10A from pyvesync.helpers import Helpers as helpers import call_json import call_json_outlets from utils import Defaults, TestBase OutletDefaults = call_json_outlets.OutletDefaults DEV_TYPE_US = 'ESW03-USA' DEV_TYPE_EU = 'ESW01-EU' DEV_LIST_DETAIL_EU = call_json.DeviceList.device_list_item(DEV_TYPE_EU) DEV_LIST_DETAIL_US = call_json.DeviceList.device_list_item(DEV_TYPE_US) CORRECT_10AUS_LIST = call_json.DeviceList.device_list_response(DEV_TYPE_US) CORRECT_10AEU_LIST = call_json.DeviceList.device_list_response(DEV_TYPE_EU) ENERGY_HISTORY = call_json_outlets.ENERGY_HISTORY CORRECT_10A_DETAILS = call_json_outlets.DETAILS_RESPONSES[DEV_TYPE_US] BAD_10A_LIST = call_json.DETAILS_BADCODE class TestVesync10ASwitch(TestBase): """Test class for 10A outlets.""" @pytest.mark.parametrize( 'mock_return, devtype', [(CORRECT_10AEU_LIST, DEV_TYPE_EU), (CORRECT_10AUS_LIST, DEV_TYPE_US)], ) def test_10a_conf(self, mock_return, devtype): """Tests that 10A US & EU Outlet is instantiated properly.""" self.mock_api.return_value = mock_return self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 1 outlet = outlets[0] assert isinstance(outlet, VeSyncOutlet10A) assert outlet.device_name == Defaults.name(devtype) assert outlet.device_type == devtype assert outlet.cid == Defaults.cid(devtype) assert outlet.uuid == Defaults.uuid(devtype) def test_10a_details(self): """Test 10A get_details().""" self.mock_api.return_value = CORRECT_10A_DETAILS outlet = VeSyncOutlet10A(DEV_LIST_DETAIL_US, self.manager) outlet.get_details() dev_details = outlet.details assert outlet.device_status == 'on' assert type(dev_details) == dict assert dev_details['active_time'] == Defaults.active_time assert dev_details['energy'] == OutletDefaults.energy assert dev_details['power'] == OutletDefaults.power assert dev_details['voltage'] == OutletDefaults.voltage assert outlet.power == OutletDefaults.power assert outlet.voltage == OutletDefaults.voltage def test_10a_details_fail(self): """Test 10A get_details with Code>0.""" self.mock_api.return_value = BAD_10A_LIST out = VeSyncOutlet10A(DEV_LIST_DETAIL_EU, self.manager) out.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_10a_onoff(self): """Test 10A Device On/Off Methods.""" self.mock_api.return_value = ({'code': 0}, 200) out = VeSyncOutlet10A(DEV_LIST_DETAIL_EU, self.manager) head = helpers.req_headers(self.manager) body = helpers.req_body(self.manager, 'devicestatus') body['status'] = 'on' body['uuid'] = out.uuid on = out.turn_on() self.mock_api.assert_called_with( '/10a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert on off = out.turn_off() body['status'] = 'off' self.mock_api.assert_called_with( '/10a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert off def test_10a_onoff_fail(self): """Test 10A On/Off Fail with Code>0.""" self.mock_api.return_value = ({'code': 1}, 400) out = VeSyncOutlet10A(DEV_LIST_DETAIL_US, self.manager) assert not out.turn_on() assert not out.turn_off() def test_10a_weekly(self): """Test 10A get_weekly_energy.""" self.mock_api.return_value = ENERGY_HISTORY out = VeSyncOutlet10A(DEV_LIST_DETAIL_EU, self.manager) out.get_weekly_energy() body = helpers.req_body(self.manager, 'energy_week') body['uuid'] = out.uuid self.mock_api.assert_called_with( '/10a/v1/device/energyweek', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = out.energy['week'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert out.weekly_energy_total == 1 def test_10a_monthly(self): """Test 10A get_monthly_energy.""" self.mock_api.return_value = ENERGY_HISTORY out = VeSyncOutlet10A(DEV_LIST_DETAIL_EU, self.manager) out.get_monthly_energy() body = helpers.req_body(self.manager, 'energy_month') body['uuid'] = out.uuid self.mock_api.assert_called_with( '/10a/v1/device/energymonth', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = out.energy['month'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert out.monthly_energy_total == 1 def test_10a_yearly(self): """Test 10A get_yearly_energy.""" self.mock_api.return_value = ENERGY_HISTORY out = VeSyncOutlet10A(DEV_LIST_DETAIL_US, self.manager) out.get_yearly_energy() body = helpers.req_body(self.manager, 'energy_year') body['uuid'] = out.uuid self.mock_api.assert_called_with( '/10a/v1/device/energyyear', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = out.energy['year'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert out.yearly_energy_total == 1 def test_history_fail(self): """Test 15A energy failure.""" bad_history = {'code': 1} self.mock_api.return_value = (bad_history, 200) out = VeSyncOutlet10A(DEV_LIST_DETAIL_US, self.manager) out.update_energy() assert len(self.caplog.records) == 1 assert 'weekly' in self.caplog.text self.caplog.clear() out.get_monthly_energy() assert len(self.caplog.records) == 1 assert 'monthly' in self.caplog.text self.caplog.clear() out.get_yearly_energy() assert len(self.caplog.records) == 1 assert 'yearly' in self.caplog.text pyvesync-2.1.18/src/tests/test_x_vesync_15a.py000066400000000000000000000166421475441503300213500ustar00rootroot00000000000000from pyvesync.vesyncoutlet import VeSyncOutlet15A from pyvesync.helpers import Helpers as helpers import call_json import utils import call_json_outlets DEVICE_TYPE = 'ESW15-USA' DEV_LIST_DETAIL = call_json.DeviceList.device_list_item(DEVICE_TYPE) CORRECT_15A_LIST = call_json.DeviceList.device_list_response(DEVICE_TYPE) ENERGY_HISTORY = call_json_outlets.ENERGY_HISTORY CORRECT_15A_DETAILS = call_json_outlets.DETAILS_RESPONSES[DEVICE_TYPE] BAD_15A_LIST = call_json.DETAILS_BADCODE class TestVeSyncSwitch(utils.TestBase): def test_15aswitch_conf(self): """Tests that 15A Outlet is instantiated properly""" self.mock_api.return_value = CORRECT_15A_LIST self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 1 vswitch15a = outlets[0] assert isinstance(vswitch15a, VeSyncOutlet15A) assert vswitch15a.device_name == call_json.Defaults.name(DEVICE_TYPE) assert vswitch15a.device_type == DEVICE_TYPE assert vswitch15a.cid == call_json.Defaults.cid(DEVICE_TYPE) assert vswitch15a.uuid == call_json.Defaults.uuid(DEVICE_TYPE) def test_15a_details(self): """Test 15A get_details() """ self.mock_api.return_value = CORRECT_15A_DETAILS vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_details() dev_details = vswitch15a.details assert vswitch15a.device_status == 'on' assert type(dev_details) == dict assert dev_details['active_time'] == 1 assert dev_details['energy'] == 1 assert dev_details['power'] == '1' assert dev_details['voltage'] == '1' assert vswitch15a.power == 1 assert vswitch15a.voltage == 1 assert vswitch15a.active_time == 1 assert vswitch15a.energy_today == 1 class TestVesync15ASwitch(utils.TestBase): def test_15aswitch_conf(self): """Tests that 15A Outlet is instantiated properly""" self.mock_api.return_value = CORRECT_15A_LIST self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 1 vswitch15a = outlets[0] assert isinstance(vswitch15a, VeSyncOutlet15A) assert vswitch15a.device_name == call_json.Defaults.name(DEVICE_TYPE) assert vswitch15a.device_type == DEVICE_TYPE assert vswitch15a.cid == call_json.Defaults.cid(DEVICE_TYPE) assert vswitch15a.uuid == call_json.Defaults.uuid(DEVICE_TYPE) def test_15a_details_fail(self): """Test 15A get_details with Code>0""" self.mock_api.return_value = BAD_15A_LIST vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_15a_no_details(self): """Test 15A details return with no details and code=0""" bad_15a_details = {'code': 0, 'deviceStatus': 'on'} self.mock_api.return_value = (bad_15a_details, 200) vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_details() assert len(self.caplog.records) == 1 def test_15a_onoff(self): """Test 15A Device On/Off Methods""" self.mock_api.return_value = ({'code': 0}, 200) vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) head = helpers.req_headers(self.manager) body = helpers.req_body(self.manager, 'devicestatus') body['status'] = 'on' body['uuid'] = vswitch15a.uuid on = vswitch15a.turn_on() self.mock_api.assert_called_with( '/15a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert on off = vswitch15a.turn_off() body['status'] = 'off' self.mock_api.assert_called_with( '/15a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert off def test_15a_onoff_fail(self): """Test 15A On/Off Fail with Code>0""" self.mock_api.return_value = ({'code': 1}, 400) vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) assert not vswitch15a.turn_on() assert not vswitch15a.turn_off() def test_15a_weekly(self): """Test 15A get_weekly_energy""" self.mock_api.return_value = ENERGY_HISTORY vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_weekly_energy() body = helpers.req_body(self.manager, 'energy_week') body['uuid'] = vswitch15a.uuid self.mock_api.assert_called_with( '/15a/v1/device/energyweek', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = vswitch15a.energy['week'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert vswitch15a.weekly_energy_total == 1 def test_15a_monthly(self): """Test 15A get_monthly_energy""" self.mock_api.return_value = ENERGY_HISTORY vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_monthly_energy() body = helpers.req_body(self.manager, 'energy_month') body['uuid'] = vswitch15a.uuid self.mock_api.assert_called_with( '/15a/v1/device/energymonth', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = vswitch15a.energy['month'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert vswitch15a.monthly_energy_total == 1 def test_15a_yearly(self): """Test 15A get_yearly_energy""" self.mock_api.return_value = ENERGY_HISTORY vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.get_yearly_energy() body = helpers.req_body(self.manager, 'energy_year') body['uuid'] = vswitch15a.uuid self.mock_api.assert_called_with( '/15a/v1/device/energyyear', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = vswitch15a.energy['year'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert vswitch15a.yearly_energy_total == 1 def test_history_fail(self): """Test 15A energy failure""" bad_history = {'code': 1} self.mock_api.return_value = (bad_history, 200) vswitch15a = VeSyncOutlet15A(DEV_LIST_DETAIL, self.manager) vswitch15a.update_energy() assert len(self.caplog.records) == 1 assert 'weekly' in self.caplog.text self.caplog.clear() vswitch15a.get_monthly_energy() assert len(self.caplog.records) == 1 assert 'monthly' in self.caplog.text self.caplog.clear() vswitch15a.get_yearly_energy() assert len(self.caplog.records) == 1 assert 'yearly' in self.caplog.text pyvesync-2.1.18/src/tests/test_x_vesync_7aswitch.py000066400000000000000000000146331475441503300225110ustar00rootroot00000000000000"""Etekcity 7A Outlet tests.""" import logging from unittest.mock import patch import pytest from pyvesync import VeSync from pyvesync.vesyncoutlet import VeSyncOutlet7A from pyvesync.helpers import Helpers as helpers import call_json import call_json_outlets from utils import Defaults, TestBase DEVICE_TYPE = 'wifi-switch-1.3' DEV_LIST_DETAIL = call_json.DeviceList.device_list_item(DEVICE_TYPE) CORRECT_7A_LIST = call_json.DeviceList.device_list_response(DEVICE_TYPE) CORRECT_7A_DETAILS = call_json_outlets.DETAILS_RESPONSES[DEVICE_TYPE] ENERGY_HISTORY = call_json_outlets.ENERGY_HISTORY DEVICE_TYPE = 'wifi-switch-1.3' CALL_LIST = [ 'turn_on', 'turn_off', 'update' ] class TestVesync7ASwitch(TestBase): """Test 7A outlet API.""" def test_7aswitch_conf(self): """Test inizialization of 7A outlet.""" self.mock_api.return_value = CORRECT_7A_LIST self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 1 vswitch7a = outlets[0] assert isinstance(vswitch7a, VeSyncOutlet7A) assert vswitch7a.device_name == call_json.Defaults.name(DEVICE_TYPE) assert vswitch7a.device_type == DEVICE_TYPE assert vswitch7a.cid == call_json.Defaults.cid(DEVICE_TYPE) assert vswitch7a.is_on def test_7a_details(self): """Test get_details() method for 7A outlet.""" self.mock_api.return_value = CORRECT_7A_DETAILS vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_details() dev_details = vswitch7a.details assert vswitch7a.device_status == 'on' assert type(dev_details) == dict assert dev_details['active_time'] == 1 assert dev_details['energy'] == 1 assert vswitch7a.power == 1 assert vswitch7a.voltage == 1 def test_7a_no_devstatus(self): """Test 7A outlet details response with no device status key.""" bad_7a_details = { 'deviceImg': '', 'activeTime': 1, 'energy': 1, 'power': '1A:1A', 'voltage': '1A:1A', } self.mock_api.return_value = (bad_7a_details, 200) vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_7a_no_details(self): """Test 7A outlet details response with unknown keys.""" bad_7a_details = {'wrongdetails': 'on'} self.mock_api.return_value = (bad_7a_details, 200) vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_details() assert len(self.caplog.records) == 1 def test_7a_onoff(self): """Test 7A outlet on/off methods.""" self.mock_api.return_value = ('response', 200) vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) on = vswitch7a.turn_on() head = helpers.req_headers(self.manager) self.mock_api.assert_called_with( '/v1/wifi-switch-1.3/' + vswitch7a.cid + '/status/on', 'put', headers=head ) assert on off = vswitch7a.turn_off() self.mock_api.assert_called_with( '/v1/wifi-switch-1.3/' + vswitch7a.cid + '/status/off', 'put', headers=head ) assert off def test_7a_onoff_fail(self): """Test 7A outlet on/off methods that fail.""" self.mock_api.return_value = ('response', 400) vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) assert not vswitch7a.turn_on() assert not vswitch7a.turn_off() def test_7a_weekly(self): """Test 7A outlet weekly energy API call and energy dict.""" self.mock_api.return_value = ENERGY_HISTORY vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_weekly_energy() self.mock_api.assert_called_with( '/v1/device/' + vswitch7a.cid + '/energy/week', 'get', headers=helpers.req_headers(self.manager), ) energy_dict = vswitch7a.energy['week'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] def test_7a_monthly(self): """Test 7A outlet monthly energy API call and energy dict.""" self.mock_api.return_value = ENERGY_HISTORY vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_monthly_energy() self.mock_api.assert_called_with( '/v1/device/' + vswitch7a.cid + '/energy/month', 'get', headers=helpers.req_headers(self.manager), ) energy_dict = vswitch7a.energy['month'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] def test_7a_yearly(self): """Test 7A outlet yearly energy API call and energy dict.""" self.mock_api.return_value = ENERGY_HISTORY vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.get_yearly_energy() self.mock_api.assert_called_with( '/v1/device/' + vswitch7a.cid + '/energy/year', 'get', headers=helpers.req_headers(self.manager), ) energy_dict = vswitch7a.energy['year'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] def test_history_fail(self): """Test handling of energy update failure.""" bad_history = {'code': 1} self.mock_api.return_value = (bad_history, 200) vswitch7a = VeSyncOutlet7A(DEV_LIST_DETAIL, self.manager) vswitch7a.update_energy() assert len(self.caplog.records) == 1 assert 'weekly' in self.caplog.text self.caplog.clear() vswitch7a.get_monthly_energy() assert len(self.caplog.records) == 1 assert 'monthly' in self.caplog.text self.caplog.clear() vswitch7a.get_yearly_energy() assert len(self.caplog.records) == 1 assert 'yearly' in self.caplog.text pyvesync-2.1.18/src/tests/test_x_vesync_bsdgo1.py000066400000000000000000000073471475441503300221430ustar00rootroot00000000000000"""Test scripts for BSDGO1 Outlets.""" import pytest from unittest.mock import patch import logging from pyvesync import VeSync from pyvesync.vesyncoutlet import VeSyncOutletBSDGO1 from pyvesync.helpers import Helpers as helpers import call_json import call_json_outlets from utils import Defaults, TestBase DEVICE_TYPE = 'BSDOG01' DEV_LIST_DETAIL = call_json.DeviceList.device_list_item(DEVICE_TYPE) CORRECT_BSDGO1_LIST = call_json.DeviceList.device_list_response(DEVICE_TYPE) CORRECT_BSDGO1_DETAILS = call_json_outlets.DETAILS_RESPONSES[DEVICE_TYPE] BAD_BSDGO1_LIST = call_json.DETAILS_BADCODE class TestVeSyncBSDGO1Switch(TestBase): """Test BSDGO1 outlet API.""" def test_bsdgo1_conf(self): """Test initialization of BSDGO1 outlet.""" self.mock_api.return_value = CORRECT_BSDGO1_LIST self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 1 bsdgo1_outlet = outlets[0] assert isinstance(bsdgo1_outlet, VeSyncOutletBSDGO1) assert bsdgo1_outlet.device_name == call_json.Defaults.name(DEVICE_TYPE) assert bsdgo1_outlet.device_type == DEVICE_TYPE assert bsdgo1_outlet.cid == call_json.Defaults.cid(DEVICE_TYPE) assert bsdgo1_outlet.uuid == call_json.Defaults.uuid(DEVICE_TYPE) def test_bsdgo1_details(self): """Test BSDGO1 get_details().""" self.mock_api.return_value = CORRECT_BSDGO1_DETAILS bsdgo1_outlet = VeSyncOutletBSDGO1(DEV_LIST_DETAIL, self.manager) bsdgo1_outlet.get_details() response = CORRECT_BSDGO1_DETAILS[0] result = response.get('result', {}) expected_status = 'on' if result.get('powerSwitch_1') == 1 else 'off' assert bsdgo1_outlet.device_status == expected_status assert result.get('active_time') == Defaults.active_time assert result.get('connectionStatus') == 'online' def test_bsdgo1_details_fail(self): """Test BSDGO1 get_details with bad response.""" self.mock_api.return_value = BAD_BSDGO1_LIST bsdgo1_outlet = VeSyncOutletBSDGO1(DEV_LIST_DETAIL, self.manager) bsdgo1_outlet.get_details() assert len(self.caplog.records) == 1 assert 'details' in self.caplog.text def test_bsdgo1_onoff(self): """Test BSDGO1 Device On/Off Methods.""" self.mock_api.return_value = ({'code': 0}, 200) bsdgo1_outlet = VeSyncOutletBSDGO1(DEV_LIST_DETAIL, self.manager) head = helpers.req_header_bypass() body = helpers.req_body(self.manager, 'bypassV2') body['cid'] = bsdgo1_outlet.cid body['configModule'] = bsdgo1_outlet.config_module # Test turn_on body['payload'] = { 'data': {'powerSwitch_1': 1}, 'method': 'setProperty', 'source': 'APP' } on = bsdgo1_outlet.turn_on() self.mock_api.assert_called_with( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=head, json_object=body ) assert on # Test turn_off body['payload'] = { 'data': {'powerSwitch_1': 0}, 'method': 'setProperty', 'source': 'APP' } off = bsdgo1_outlet.turn_off() self.mock_api.assert_called_with( '/cloud/v2/deviceManaged/bypassV2', 'post', headers=head, json_object=body ) assert off def test_bsdgo1_onoff_fail(self): """Test BSDGO1 On/Off Fail with bad response.""" self.mock_api.return_value = BAD_BSDGO1_LIST bsdgo1_outlet = VeSyncOutletBSDGO1(DEV_LIST_DETAIL, self.manager) assert not bsdgo1_outlet.turn_on() assert not bsdgo1_outlet.turn_off() pyvesync-2.1.18/src/tests/test_x_vesync_bulbs.py000066400000000000000000000315231475441503300220640ustar00rootroot00000000000000from collections import namedtuple from pyvesync.helpers import RGB from pyvesync.vesyncbulb import (VeSyncBulbESL100, VeSyncBulbESL100CW, VeSyncBulbESL100MC, VeSyncBulbValcenoA19MC) import call_json import call_json_bulbs from utils import TestBase DEV_LIST = call_json.DeviceList.device_list_response('ESL100') DEV_LIST_DETAIL = call_json.DeviceList.device_list_item('ESL100') DEV_LIST_DETAIL_CW = call_json.DeviceList.device_list_item('ESL100CW') DEV_LIST_CW = call_json.DeviceList.device_list_response('ESL100CW') DEV_LIST_DETAIL_MC = call_json.DeviceList.device_list_item('ESL100MC') DEV_LIST_MC = call_json.DeviceList.device_list_response('ESL100MC') DEV_LIST_DETAIL_VALCENO = call_json.DeviceList.device_list_item('XYD0001') DEV_LIST_VALCENO = call_json.DeviceList.device_list_response('XYD0001') DEVICE_DETAILS = call_json_bulbs.DETAILS_RESPONSES['ESL100'] DEVICE_DETAILS_CW = call_json_bulbs.DETAILS_RESPONSES['ESL100CW'] DEFAULTS = call_json.Defaults class TestVeSyncBulbESL100(TestBase): """Tests for VeSync dimmable bulb.""" device_type = 'ESL100' def test_esl100_conf(self): """Tests that Wall Switch is instantiated properly.""" self.mock_api.return_value = DEV_LIST self.manager.get_devices() bulbs = self.manager.bulbs assert len(bulbs) == 1 bulb = bulbs[0] assert isinstance(bulb, VeSyncBulbESL100) assert bulb.device_name == call_json.Defaults.name(self.device_type) assert bulb.device_type == self.device_type assert bulb.cid == call_json.Defaults.cid(self.device_type) assert bulb.uuid == call_json.Defaults.uuid(self.device_type) def test_esl100_details(self): """Test WS get_details().""" if callable(DEVICE_DETAILS): self.mock_api.return_value = DEVICE_DETAILS() else: self.mock_api.return_value = DEVICE_DETAILS bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) bulb.get_details() dev_details = bulb.details assert bulb.device_status == 'on' assert isinstance(dev_details, dict) assert bulb.connection_status == 'online' def test_esl100_no_details(self): """Test no device details for disconnected bulb.""" self.mock_api.return_value = ({'code': 5}, 200) bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) bulb.update() assert len(self.caplog.records) == 1 def test_esl100_onoff(self): """Test power toggle for ESL100 bulb.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) assert bulb.turn_off() assert bulb.turn_on() def test_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) assert bulb.set_brightness(50) assert bulb.turn_off() assert bulb.set_brightness(50) assert bulb.device_status == 'on' def test_invalid_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) assert bulb.set_brightness(5000) assert bulb.brightness == 100 def test_features(self): bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager) assert bulb.dimmable_feature assert not bulb.color_temp_feature assert not bulb.rgb_shift_feature class TestVeSyncBulbESL100CW(TestBase): """Tests for VeSync dimmable bulb.""" device_type = 'ESL100CW' def test_esl100cw_conf(self): """Tests that Wall Switch is instantiated properly.""" if callable(DEV_LIST_CW): self.mock_api.return_value = DEV_LIST_CW() else: self.mock_api.return_value = DEV_LIST_CW self.manager.get_devices() bulbs = self.manager.bulbs assert len(bulbs) == 1 bulb = bulbs[0] assert isinstance(bulb, VeSyncBulbESL100CW) assert bulb.device_name == DEFAULTS.name(self.device_type) assert bulb.device_type == self.device_type assert bulb.cid == DEFAULTS.cid(self.device_type) assert bulb.uuid == DEFAULTS.uuid(self.device_type) def test_esl100cw_details(self): """Test WS get_details().""" if callable(DEVICE_DETAILS_CW): self.mock_api.return_value = DEVICE_DETAILS_CW() else: self.mock_api.return_value = DEVICE_DETAILS_CW bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) bulb.get_details() assert self.mock_api.r assert bulb.device_status == 'on' assert bulb.connection_status == 'online' def test_esl100cw_onoff(self): """Test power toggle for ESL100 bulb.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) assert bulb.turn_off() assert bulb.device_status == 'off' assert bulb.turn_on() assert bulb.device_status == 'on' def test_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) assert bulb.set_brightness(50) assert bulb.brightness == 50 assert bulb.turn_off() assert bulb.set_brightness(50) assert bulb.device_status == 'on' def test_invalid_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) assert bulb.set_brightness(5000) assert bulb.brightness == 100 def test_color_temp(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) assert bulb.set_color_temp(50) assert bulb.color_temp_pct == 50 def test_features(self): bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager) assert bulb.dimmable_feature assert bulb.color_temp_feature assert not bulb.rgb_shift_feature class TestVeSyncBulbESL100MC(TestBase): """Tests for VeSync dimmable bulb.""" device_type = 'ESL100MC' def test_esl100mc_conf(self): """Tests that Wall Switch is instantiated properly.""" self.mock_api.return_value = DEV_LIST_MC self.manager.get_devices() bulbs = self.manager.bulbs assert len(bulbs) == 1 bulb = bulbs[0] assert isinstance(bulb, VeSyncBulbESL100MC) assert bulb.device_name == DEFAULTS.name(self.device_type) assert bulb.device_type == self.device_type assert bulb.cid == DEFAULTS.cid(self.device_type) assert bulb.uuid == DEFAULTS.uuid(self.device_type) def test_esl100mc_details(self): """Test WS get_details().""" self.mock_api.return_value = DEV_LIST_MC bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) bulb.get_details() assert self.mock_api.r assert bulb.device_status == 'on' assert bulb.connection_status == 'online' def test_esl100mc_onoff(self): """Test power toggle for ESL100 bulb.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) assert bulb.turn_off() assert bulb.device_status == 'off' assert bulb.turn_on() assert bulb.device_status == 'on' def test_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) assert bulb.set_brightness(50) assert bulb.brightness == 50 assert bulb.turn_off() assert bulb.set_brightness(50) assert bulb.device_status == 'on' def test_invalid_brightness(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) assert bulb.set_brightness(5000) assert bulb.brightness == 100 def test_color(self): self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) assert bulb.set_rgb(50, 100, 150) assert bulb.color_rgb == namedtuple('rgb', 'red green blue')(50, 100, 150) def test_features(self): bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager) assert bulb.dimmable_feature assert not bulb.color_temp_feature assert bulb.rgb_shift_feature class TestVeSyncBulbValceno(TestBase): """Tests for VeSync Valceno bulb.""" device_type = 'XYD0001' def test_valceno_conf(self): """Tests that Valceno is instantiated properly.""" self.mock_api.return_value = DEV_LIST_VALCENO self.manager.get_devices() bulbs = self.manager.bulbs assert len(bulbs) == 1 bulb = bulbs[0] assert isinstance(bulb, VeSyncBulbValcenoA19MC) assert bulb.device_name == DEFAULTS.name(self.device_type) assert bulb.device_type == self.device_type assert bulb.cid == DEFAULTS.cid(self.device_type) assert bulb.uuid == DEFAULTS.uuid(self.device_type) def test_valceno_details(self): """Test Valceno get_details().""" self.mock_api.return_value = DEV_LIST_VALCENO bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) bulb.get_details() assert self.mock_api.r assert bulb.device_status == 'on' assert bulb.connection_status == 'online' def test_valceno_onoff(self): """Test power toggle for Valceno MC bulb.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.turn_off() assert bulb.device_status == 'off' assert bulb.turn_on() assert bulb.device_status == 'on' def test_brightness(self): """Test brightness on Valceno.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.set_brightness(50) assert bulb.brightness == 50 assert bulb.turn_off() assert bulb.set_brightness(50) assert bulb.device_status == 'on' def test_invalid_brightness(self): """Test invalid brightness on Valceno.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.set_brightness(5000) assert bulb.brightness == 100 def test_invalid_saturation(self): """Test invalid saturation on Valceno.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.set_color_saturation(5000) body_dict = { "method": "setLightStatusV2", "source": "APP", "data": { "force": 1, "brightness": "", "colorTemp": "", "colorMode": "hsv", "hue": "", "saturation": 10000, "value": "" } } mock_call = self.mock_api.call_args[1]['json_object']['payload'] assert mock_call == body_dict def test_color(self): """Test set color on Valceno.""" self.mock_api.return_value = ({ 'code': 0, 'msg': '', 'result': { 'code': 0, 'result': { "enabled": 'on', "colorMode": 'hsv', 'brightness': 100, 'hue': 5833, 'saturation': 6700, 'value': 59 } } }, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.set_rgb(50, 100, 150) assert bulb.color_rgb == RGB(50, 100, 150) def test_hue(self): """Test hue on Valceno MC Bulb.""" self.mock_api.return_value = ({'code': 0}, 200) bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) bulb.set_color_hue(230.5) body_dict = { "method": "setLightStatusV2", "source": "APP", "data": { "force": 1, "brightness": "", "colorTemp": "", "colorMode": "hsv", "hue": 6403, "saturation": "", "value": "" } } mock_call = self.mock_api.call_args[1]['json_object']['payload'] assert mock_call == body_dict def test_features(self): bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager) assert bulb.dimmable_feature assert bulb.color_temp_feature assert bulb.rgb_shift_feature pyvesync-2.1.18/src/tests/test_x_vesync_devices.py000066400000000000000000000271631475441503300224040ustar00rootroot00000000000000"""Test VeSync manager methods.""" import pytest from unittest.mock import patch import pyvesync import logging import copy import time from itertools import chain from pyvesync.vesyncfan import * from pyvesync.vesyncbulb import * from pyvesync.vesyncoutlet import * from pyvesync.vesyncswitch import * import call_json as json_vals from call_json_switches import SWITCHES_NUM from call_json_outlets import OUTLETS_NUM from call_json_fans import FANS_NUM from call_json_bulbs import BULBS_NUM BAD_DEV_LIST = { 'result': { 'total': 5, 'pageNo': 1, 'pageSize': 50, 'list': [{'NoConfigKeys': None}], }, 'code': 0, } class TestDeviceList(object): """Test getting and populating device lists.""" @pytest.fixture(scope='function') def api_mock(self, caplog): """Mock call_api and initialize VeSync object.""" self.mock_api_call = patch('pyvesync.helpers.Helpers.call_api') self.mock_api = self.mock_api_call.start() self.mock_api.create_autospec() self.mock_api.return_value.ok = True self.vesync_obj = pyvesync.vesync.VeSync('sam@mail.com', 'pass', debug=True) self.vesync_obj.enabled = True self.vesync_obj.token = 'sample_tk' self.vesync_obj.account_id = 'sample_id' self.vesync_obj.in_process = False caplog.set_level(logging.DEBUG) yield self.mock_api_call.stop() def test_device_api(self, caplog, api_mock): """Tests to ensure call_api is being called correctly.""" head = json_vals.DEFAULT_HEADER_BYPASS self.mock_api.return_value = ({'V': 2}, 200) self.vesync_obj.get_devices() call_list = self.mock_api.call_args_list call_p1 = call_list[0][0] call_p2 = call_list[0][1] assert call_p1[0] == '/cloud/v1/deviceManaged/devices' assert call_p1[1] == 'post' assert call_p2['headers'] == head assert self.vesync_obj.enabled @patch('pyvesync.vesync.VeSyncOutlet7A') @patch('pyvesync.vesync.VeSyncOutlet15A') @patch('pyvesync.vesync.VeSyncOutlet10A') @patch('pyvesync.vesync.VeSyncWallSwitch') @patch('pyvesync.vesync.VeSyncDimmerSwitch') @patch('pyvesync.vesync.VeSyncAir131') def test_getdevs_vsfact( self, air_patch, wsdim_patch, ws_patch, out10a_patch, out15a_patch, out7a_patch, api_mock ): """Test the get_devices, process_devices and VSFactory methods. Build list with device objects from details Test for all 6 known devices - 4 outlets, 2 switches, 1 fan. """ device_list = json_vals.DeviceList.device_list_response() self.mock_api.return_value = device_list self.vesync_obj.get_devices() assert len(self.vesync_obj.outlets) == OUTLETS_NUM assert len(self.vesync_obj.switches) == SWITCHES_NUM assert len(self.vesync_obj.fans) == FANS_NUM assert len(self.vesync_obj.bulbs) == BULBS_NUM def test_lv600( self, api_mock ): """Test the get_devices, process_devices and VSFactory methods. Build list with device objects from details Test for all 6 known devices - 4 outlets, 2 switches, 1 fan. """ device_list = json_vals.DeviceList.FAN_TEST self.mock_api.return_value = device_list self.vesync_obj.get_devices() assert len(self.vesync_obj.fans) == 3 def test_dual200s( self, api_mock ): """Test the get_devices, process_devices and VSFactory methods. Build list with device objects from details Test for all 6 known devices - 4 outlets, 2 switches, 1 fan. """ device_list = json_vals.DeviceList.device_list_response('Dual200S') self.mock_api.return_value = device_list self.vesync_obj.debug = True self.vesync_obj.get_devices() assert len(self.vesync_obj.fans) == 1 def test_getdevs_code(self, caplog, api_mock): """Test get_devices with code > 0 returned.""" device_list = ({'code': 1, 'msg': 'gibberish'}, 200) self.mock_api.return_value = device_list self.vesync_obj.get_devices() assert 'Error retrieving device list' in caplog.text def test_get_devices_resp_changes(self, caplog, api_mock): """Test if structure of device list response has changed.""" device_list = ( { 'code': 0, 'NOTresult': { 'NOTlist': [ { 'deviceType': 'wifi-switch-1.3', 'type': 'wifi-switch', 'cid': 'cid1', } ] }, }, 200, ) self.mock_api.return_value = device_list self.vesync_obj.get_devices() assert len(caplog.records) == 1 assert 'Device list in response not found' in caplog.text def test_7a_bad_conf(self, caplog, api_mock): """Test bad device list response.""" self.mock_api.return_value = (BAD_DEV_LIST, 200) self.vesync_obj.get_devices() assert len(caplog.records) == 2 def test_7a_no_dev_list(self, caplog, api_mock): """Test if empty device list is handled correctly.""" empty_list = [] self.vesync_obj.process_devices(empty_list) assert len(caplog.records) == 1 def test_get_devices_devicetype_error(self, caplog, api_mock): """Test result and list keys exist but deviceType not in list.""" device_list = ( {'code': 0, 'result': {'list': [{'type': 'wifi-switch', 'cid': 'cid1'}]}}, 200, ) self.mock_api.return_value = device_list self.vesync_obj.get_devices() assert len(caplog.records) == 2 assert 'Error adding device' in caplog.text def test_unknown_device(self, caplog, api_mock): """Test unknown device type is handled correctly.""" unknown_dev = json_vals.DeviceList.LIST_CONF_7A unknown_dev['devType'] = 'UNKNOWN-DEVTYPE' pyvesync.vesync.object_factory('unknown_device', unknown_dev, self.vesync_obj) assert len(caplog.records) == 1 assert 'Unknown' in caplog.text def test_time_check(self, api_mock): """Test device details update throttle.""" time_check = self.vesync_obj.device_time_check() assert time_check is True self.vesync_obj.last_update_ts = time.time() time_check = self.vesync_obj.device_time_check() assert time_check is False self.vesync_obj.last_update_ts = ( time.time() - self.vesync_obj.update_interval - 1 ) time_check = self.vesync_obj.device_time_check() assert time_check is True @patch('pyvesync.vesync.VeSyncOutlet7A', autospec=True) def test_remove_device(self, outlet_patch, caplog, api_mock): """Test remove device test.""" device = copy.deepcopy(json_vals.DeviceList.LIST_CONF_7A) outlet_test = outlet_patch.return_value outlet_test.cid = '7A-CID' outlet_test.device_type = 'wifi-switch-1.3' outlet_test.device_name = '7A Device' new_list = [device] self.vesync_obj.outlets = [outlet_test] device_exists = pyvesync.vesync.VeSync.remove_old_devices( self.vesync_obj, new_list ) assert device_exists del device['cid'] device_exists = pyvesync.vesync.VeSync.remove_dev_test(outlet_test, new_list) assert device_exists is False assert len(caplog.records) == 2 assert 'cid' in caplog.text @patch('pyvesync.vesync.VeSyncOutdoorPlug', autospec=True) def test_add_dev_test(self, outdoor_patch, caplog, api_mock): """Test add_device_test to return if device found in existing conf.""" outdoor_inst = VeSyncOutdoorPlug( json_vals.DeviceList.LIST_CONF_OUTDOOR_2, self.vesync_obj ) self.vesync_obj.outlets = [outdoor_inst] add_test = self.vesync_obj.add_dev_test(json_vals.DeviceList.LIST_CONF_OUTDOOR_1) assert add_test def test_display_func(self, caplog, api_mock): """Test display function outputs text.""" self.vesync_obj.outlets.append( VeSyncOutdoorPlug(json_vals.DeviceList.device_list_item('ESO15-TB', 0), self.vesync_obj) ) self.vesync_obj.outlets.append( VeSyncOutlet10A(json_vals.DeviceList.device_list_item('ESW01-EU'), self.vesync_obj) ) self.vesync_obj.outlets.append( VeSyncOutlet15A(json_vals.DeviceList.device_list_item('ESW15-USA'), self.vesync_obj) ) self.vesync_obj.outlets.append( VeSyncOutlet7A(json_vals.DeviceList.LIST_CONF_7A, self.vesync_obj) ) self.vesync_obj.switches.append( VeSyncWallSwitch(json_vals.DeviceList.LIST_CONF_WS, self.vesync_obj) ) self.vesync_obj.fans.append( VeSyncAir131(json_vals.DeviceList.LIST_CONF_AIR, self.vesync_obj) ) self.vesync_obj.bulbs.append( VeSyncBulbESL100(json_vals.DeviceList.LIST_CONF_ESL100, self.vesync_obj) ) dev_list = [ self.vesync_obj.outlets, self.vesync_obj.switches, self.vesync_obj.fans, self.vesync_obj.bulbs, ] for device in chain(*dev_list): device.display() assert len(caplog.records) == 0 @patch('pyvesync.vesync.VeSyncOutlet7A', autospec=True) @patch('pyvesync.vesync.VeSyncOutlet15A', autospec=True) @patch('pyvesync.vesync.VeSyncOutlet10A', autospec=True) @patch('pyvesync.vesync.VeSyncOutdoorPlug', autospec=True) @patch('pyvesync.vesync.VeSyncBulbESL100', autospec=True) @patch('pyvesync.vesync.VeSyncWallSwitch', autospec=True) @patch('pyvesync.vesync.VeSyncAir131', autospec=True) def test_resolve_updates( self, air_patch, ws_patch, esl100_patch, outdoor_patch, out10a_patch, out15a_patch, out7a_patch, caplog, api_mock, ): """Test process_devices() with all devices. Creates vesync object with all devices and returns device list with new set of all devices. """ out10a_patch.cid = '10A-CID1' out10a_patch.device_type = 'ESW10-EU' out10a_patch.device_name = '10A Removed' out15a_patch.cid = '15A-CID1' out15a_patch.device_type = 'ESW15-USA' out15a_patch.device_name = '15A Removed' out7a_patch.cid = '7A-CID1' out7a_patch.device_type = 'wifi-switch-1.3' out7a_patch.device_name = '7A Removed' outdoor_patch.cid = 'OUTDOOR-CID1' outdoor_patch.device_type = 'ESO15-TB' outdoor_patch.device_name = 'Outdoor Removed' esl100_patch.cid = 'BULB-CID1' esl100_patch.device_type = 'ESL100' esl100_patch.device_name = 'Bulb Removed' ws_patch.cid = 'WS-CID2' ws_patch.device_name = 'Switch Removed' ws_patch.device_type = 'ESWL01' air_patch.cid = 'AirCID2' air_patch.device_type = 'LV-PUR131S' air_patch.device_name = 'fan Removed' json_ret = json_vals.DeviceList.FULL_DEV_LIST self.vesync_obj.outlets.extend( [out7a_patch, out10a_patch, outdoor_patch] ) self.vesync_obj.switches.extend([ws_patch]) self.vesync_obj.fans.extend([air_patch]) self.vesync_obj.bulbs.extend([esl100_patch]) self.vesync_obj.process_devices(json_ret) assert len(self.vesync_obj.outlets) == 6 assert len(self.vesync_obj.switches) == 2 assert len(self.vesync_obj.fans) == 4 assert len(self.vesync_obj.bulbs) == 4 pyvesync-2.1.18/src/tests/test_x_vesync_login.py000066400000000000000000000056611475441503300220710ustar00rootroot00000000000000"""Test VeSync login method.""" import logging import pytest from unittest.mock import patch import pyvesync from pyvesync.vesync import VeSync from pyvesync.helpers import Helpers as helpers login_test_vals = [ ('sam@mail.com', 'pass', 'America/New_York', 'full corret'), ('sam@mail.com', 'pass', 'invalidtz!', 'invalid tz'), ('sam@mail.com', 'pass', None, 'none tz'), ('sam@mail.com', 'pass', '', 'empty tz'), ('sam@mail.com', None, None, 'none tz pass'), ('sam@mail.com', '', '', 'empty pass'), ] @pytest.mark.parametrize('email, password, timezone, testid', login_test_vals) def test_vesync_init(email, password, timezone, testid): """Testing only input validation.""" v_inst = VeSync(email, password, timezone) assert isinstance(v_inst, VeSync) assert v_inst.username == email assert v_inst.password == password if testid == 'full correct': assert v_inst.time_zone == timezone elif testid in ('invalid tz', 'none tz', 'non tz pass', 'empty tz'): assert v_inst.time_zone == pyvesync.helpers.DEFAULT_TZ login_bad_call = [ ('sam@mail.com', 'pass', 'correct'), ('sam@mail.com', '', 'empty pass'), ('sam@mail.com', None, 'none pass'), ('', 'pass', 'empty email'), (None, 'pass', 'none email'), ] class TestLogin(object): """Test VeSync login class.""" @pytest.fixture() def api_mock(self, caplog): """Mock call_api and initialize VeSync device.""" self.mock_api_call = patch('pyvesync.helpers.Helpers.call_api') self.mock_api = self.mock_api_call.start() self.mock_api.create_autospec() self.mock_api.return_value.ok = True caplog.set_level(logging.DEBUG) yield self.mock_api_call.stop() @pytest.mark.parametrize('email, password, testid', login_bad_call) def test_bad_login(self, api_mock, email, password, testid): """Test failed login.""" full_return = ({'code': 455}, 200) self.mock_api.return_value = full_return vesync_obj = VeSync(email, password) assert vesync_obj.login() is False if testid == 'correct': jd = helpers.req_body(vesync_obj, 'login') self.mock_api.assert_called_with('/cloud/v1/user/login', 'post', json_object=jd) else: assert not self.mock_api.called def test_good_login(self, api_mock): """Test successful login.""" full_return = ( {'code': 0, 'result': {'accountID': 'sam_actid', 'token': 'sam_token'}}, 200, ) self.mock_api.return_value = full_return vesync_obj = VeSync('sam@mail.com', 'pass') assert vesync_obj.login() is True jd = helpers.req_body(vesync_obj, 'login') self.mock_api.assert_called_with('/cloud/v1/user/login', 'post', json_object=jd) assert vesync_obj.token == 'sam_token' assert vesync_obj.account_id == 'sam_actid' pyvesync-2.1.18/src/tests/test_x_vesync_outdoor.py000066400000000000000000000157341475441503300224560ustar00rootroot00000000000000"""Test scripts for Etekcity Outdoor Outlet.""" from typing import Any, Dict, Union from copy import deepcopy from pyvesync.vesyncoutlet import VeSyncOutdoorPlug from pyvesync.helpers import Helpers as helpers import call_json import call_json_outlets from utils import TestBase, Defaults DEVICE_TYPE = 'ESO15-TB' DEV_LIST_DETAIL: Dict[str, Union[str, int, float] ] = call_json.DeviceList.device_list_item(DEVICE_TYPE, 0) DEV_LIST_DETAIL_2: Dict[str, Any] = call_json.DeviceList.device_list_item(DEVICE_TYPE, 1) CORRECT_OUTDOOR_LIST: Dict[str, Any] = deepcopy(call_json.DeviceList.list_response_base) CORRECT_OUTDOOR_LIST['result']['list'].extend([DEV_LIST_DETAIL, DEV_LIST_DETAIL_2]) CORRECT_OUTDOOR_RESP: tuple = (CORRECT_OUTDOOR_LIST, 200) ENERGY_HISTORY: tuple = call_json_outlets.ENERGY_HISTORY CORRECT_OUTDOOR_DETAILS = call_json_outlets.DETAILS_RESPONSES[DEVICE_TYPE] BAD_OUTDOOR_LIST: tuple = call_json.DETAILS_BADCODE DEFAULTS = Defaults class TestVesyncOutdoorPlug(TestBase): """Test class for outdoor outlet.""" def test_outdoor_conf(self): """Tests outdoor outlet is instantiated properly.""" self.mock_api.return_value = CORRECT_OUTDOOR_RESP self.manager.get_devices() outlets = self.manager.outlets assert len(outlets) == 2 outdoor_outlet = outlets[0] assert isinstance(outdoor_outlet, VeSyncOutdoorPlug) assert outdoor_outlet.device_type == DEVICE_TYPE assert outdoor_outlet.uuid == DEFAULTS.uuid(DEVICE_TYPE) def test_outdoor_details(self): """Tests retrieving outdoor outlet details.""" self.mock_api.return_value = CORRECT_OUTDOOR_DETAILS outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.get_details() dev_details = outdoor_outlet.details assert outdoor_outlet.device_status == 'on' assert isinstance(outdoor_outlet, VeSyncOutdoorPlug) assert dev_details['active_time'] == 1 def test_outdoor_details_fail(self, caplog): """Test outdoor outlet get_details response.""" self.mock_api.return_value = BAD_OUTDOOR_LIST outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.get_details() assert len(caplog.records) == 1 assert 'details' in caplog.text def test_outdoor_outlet_onoff(self): """Test Outdoor Outlet Device On/Off Methods.""" self.mock_api.return_value = ({'code': 0}, 200) outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) head = helpers.req_headers(self.manager) body = helpers.req_body(self.manager, 'devicestatus') body['status'] = 'on' body['uuid'] = outdoor_outlet.uuid body['switchNo'] = outdoor_outlet.sub_device_no on = outdoor_outlet.turn_on() self.mock_api.assert_called_with( '/outdoorsocket15a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert on off = outdoor_outlet.turn_off() body['status'] = 'off' self.mock_api.assert_called_with( '/outdoorsocket15a/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert off def test_outdoor_outlet_onoff_fail(self): """Test outdoor outlet On/Off Fail with Code>0.""" self.mock_api.return_value = ({'code': 1}, 400) outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) assert not outdoor_outlet.turn_on() assert not outdoor_outlet.turn_off() def test_outdoor_outlet_weekly(self): """Test outdoor outlet get_weekly_energy.""" self.mock_api.return_value = ENERGY_HISTORY outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.get_weekly_energy() body = helpers.req_body(self.manager, 'energy_week') body['uuid'] = outdoor_outlet.uuid self.mock_api.assert_called_with( '/outdoorsocket15a/v1/device/energyweek', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = outdoor_outlet.energy['week'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert outdoor_outlet.weekly_energy_total == 1 def test_outdoor_outlet_monthly(self): """Test outdoor outlet get_monthly_energy.""" self.mock_api.return_value = ENERGY_HISTORY outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.get_monthly_energy() body = helpers.req_body(self.manager, 'energy_month') body['uuid'] = outdoor_outlet.uuid self.mock_api.assert_called_with( '/outdoorsocket15a/v1/device/energymonth', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = outdoor_outlet.energy['month'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert outdoor_outlet.monthly_energy_total == 1 def test_outdoor_outlet_yearly(self): """Test outdoor outlet get_yearly_energy.""" self.mock_api.return_value = ENERGY_HISTORY outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.get_yearly_energy() body = helpers.req_body(self.manager, 'energy_year') body['uuid'] = outdoor_outlet.uuid self.mock_api.assert_called_with( '/outdoorsocket15a/v1/device/energyyear', 'post', headers=helpers.req_headers(self.manager), json_object=body, ) energy_dict = outdoor_outlet.energy['year'] assert energy_dict['energy_consumption_of_today'] == 1 assert energy_dict['cost_per_kwh'] == 1 assert energy_dict['max_energy'] == 1 assert energy_dict['total_energy'] == 1 assert energy_dict['data'] == [1, 1] assert outdoor_outlet.yearly_energy_total == 1 def test_history_fail(self): """Test outdoor outlet energy failure.""" bad_history = {'code': 1} self.mock_api.return_value = (bad_history, 200) outdoor_outlet = VeSyncOutdoorPlug(DEV_LIST_DETAIL, self.manager) outdoor_outlet.update_energy() assert len(self.caplog.records) == 1 assert 'weekly' in self.caplog.text self.caplog.clear() outdoor_outlet.get_monthly_energy() assert len(self.caplog.records) == 1 assert 'monthly' in self.caplog.text self.caplog.clear() outdoor_outlet.get_yearly_energy() assert len(self.caplog.records) == 1 assert 'yearly' in self.caplog.text pyvesync-2.1.18/src/tests/test_x_wall_switch.py000066400000000000000000000071161475441503300217070ustar00rootroot00000000000000import pytest from unittest.mock import patch import logging from pyvesync import VeSync from pyvesync.vesyncswitch import VeSyncWallSwitch from pyvesync.helpers import Helpers as helpers import call_json import call_json_switches DEVICE_TYPE = 'ESWL01' DEV_LIST_DETAIL = call_json.DeviceList.device_list_item(DEVICE_TYPE) CORRECT_WS_LIST = call_json.DeviceList.device_list_response(DEVICE_TYPE) CORRECT_WS_DETAILS = call_json_switches.DETAILS_RESPONSES[DEVICE_TYPE] BAD_LIST = call_json.DETAILS_BADCODE DEFAULTS = call_json.Defaults class TestVesyncWallSwitch(object): @pytest.fixture() def api_mock(self, caplog): self.mock_api_call = patch('pyvesync.helpers.Helpers.call_api') self.mock_api = self.mock_api_call.start() self.mock_api.create_autospect() self.mock_api.return_value.ok = True self.vesync_obj = VeSync('sam@mail.com', 'pass', debug=True) self.vesync_obj.enabled = True self.vesync_obj.login = True self.vesync_obj.token = DEFAULTS.token self.vesync_obj.account_id = DEFAULTS.account_id caplog.set_level(logging.DEBUG) yield self.mock_api_call.stop() def test_ws_conf(self, api_mock): """Tests that Wall Switch is instantiated properly""" self.mock_api.return_value = CORRECT_WS_LIST self.vesync_obj.get_devices() switch = self.vesync_obj.switches assert len(switch) == 1 wswitch = switch[0] assert isinstance(wswitch, VeSyncWallSwitch) assert wswitch.device_name == DEFAULTS.name(DEVICE_TYPE) assert wswitch.device_type == DEVICE_TYPE assert wswitch.cid == DEFAULTS.cid(DEVICE_TYPE) assert wswitch.uuid == DEFAULTS.uuid(DEVICE_TYPE) def test_ws_details(self, api_mock): """Test WS get_details() """ self.mock_api.return_value = CORRECT_WS_DETAILS wswitch = VeSyncWallSwitch(DEV_LIST_DETAIL, self.vesync_obj) wswitch.get_details() dev_details = wswitch.details assert wswitch.device_status == 'on' assert type(dev_details) == dict assert dev_details['active_time'] == 1 assert wswitch.connection_status == 'online' def test_ws_details_fail(self, caplog, api_mock): """Test WS get_details with Code>0""" self.mock_api.return_value = BAD_LIST vswitch15a = VeSyncWallSwitch(DEV_LIST_DETAIL, self.vesync_obj) vswitch15a.get_details() assert len(caplog.records) == 1 assert 'details' in caplog.text def test_ws_onoff(self, caplog, api_mock): """Test 15A Device On/Off Methods""" self.mock_api.return_value = ({'code': 0}, 200) wswitch = VeSyncWallSwitch(DEV_LIST_DETAIL, self.vesync_obj) head = helpers.req_headers(self.vesync_obj) body = helpers.req_body(self.vesync_obj, 'devicestatus') body['status'] = 'on' body['uuid'] = wswitch.uuid on = wswitch.turn_on() self.mock_api.assert_called_with( '/inwallswitch/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert on off = wswitch.turn_off() body['status'] = 'off' self.mock_api.assert_called_with( '/inwallswitch/v1/device/devicestatus', 'put', headers=head, json_object=body ) assert off def test_ws_onoff_fail(self, api_mock): """Test ws On/Off Fail with Code>0""" self.mock_api.return_value = ({'code': 1}, 400) vswitch15a = VeSyncWallSwitch(DEV_LIST_DETAIL, self.vesync_obj) assert not vswitch15a.turn_on() assert not vswitch15a.turn_off() pyvesync-2.1.18/src/tests/utils.py000066400000000000000000000311201475441503300171310ustar00rootroot00000000000000"""Common base class for tests and Default values. Routine Listings ---------------- FunctionResponses : defaultdict Defaultdict of the standard response tuple for device methods Defaults: class Default values and methods for generating default values TestBase: class Base class for tests to start mock & instantiat VS object """ import logging from pathlib import Path from typing import Any import pytest import yaml from collections import defaultdict, namedtuple from unittest.mock import patch from requests.structures import CaseInsensitiveDict from pyvesync.vesync import VeSync from pyvesync.helpers import Color import pyvesync.helpers as vs_helpers logger = logging.getLogger(__name__) FunctionResponses: defaultdict = defaultdict(lambda: ({"code": 0, "msg": None}, 200)) CALL_API_ARGS = ['url', 'method', 'data', 'headers'] ID_KEYS = ['CID', 'UUID', 'MACID'] class Defaults: """General defaults for API responses and requests. Attributes ---------- token : str Default token for API requests active_time : str Default active time for API responses account_id : str Default account ID for API responses active_time : str Default active time for API responses color: Color (dataclass) Red=50, Green=100, Blue=225, Hue=223, Saturation=77.78, value=88.24 Default Color dataclass contains red, green, blue, hue, saturation and value attributes Methods -------- name(dev_type='NA') Default device name created from "dev_type-NAME" cid(dev_type='NA') Default device cid created from "dev_type-CID" uuid(dev_type='NA') Default device uuid created from "dev_type-UUID" macid(dev_type='NA') Default device macid created from "dev_type-MACID" """ token = 'sample_tk' account_id = 'sample_id' trace_id = "TRACE_ID" active_time = 1 color = Color(red=50, green=100, blue=225) brightness = 100 color_temp = 100 bool_toggle = True str_toggle = 'on' bin_toggle = 1 @staticmethod def name(dev_type: str = 'NA'): """Name of device with format f"{dev_type}-NAME". Parameters ---------- dev_type : str Device type use to create default name Returns ------- str Default name for device f"dev_type-NAME" """ return f'{dev_type}-NAME' @staticmethod def cid(dev_type='NA'): """CID for a device with format f"{dev_type}-CID". Parameters ---------- dev_type : str Device type use to create default cid Returns ------- str Default cid for device f"dev_type-CID" """ return f'{dev_type}-CID' @staticmethod def uuid(dev_type='NA'): """UUID for a device with format f"{dev_type}-UUID". Parameters ---------- dev_type : str Device type use to create default UUID Returns ------- str Default uuid for device f"{dev_type}-UUID" """ return f'{dev_type}-UUID' @staticmethod def macid(dev_type='NA'): """MACID for a device with format f"{dev_type}-MACID". Parameters ---------- dev_type : str Device type use to create default macid Returns ------- str Default macID for device f"{dev_type}-MACID" """ return f'{dev_type}-MACID' def parse_args(mock_api): """Parse arguments from mock API call. Arguments ---------- mock_api : mock Mock object used to path call_api() method Returns ------- dict dictionary of all call_api() arguments """ call_args = mock_api.call_args.args call_kwargs = mock_api.call_args.kwargs all_kwargs = dict(zip(CALL_API_ARGS, call_args)) all_kwargs.update(call_kwargs) return all_kwargs API_DEFAULTS = CaseInsensitiveDict({ 'accountID': Defaults.account_id, 'token': Defaults.token, 'timeZone': vs_helpers.DEFAULT_TZ, 'acceptLanguage': 'en', 'appVersion': vs_helpers.APP_VERSION, 'phoneBrand': vs_helpers.PHONE_BRAND, 'phoneOS': vs_helpers.PHONE_OS, 'userType': vs_helpers.USER_TYPE, "tk": Defaults.token, "traceId": "TRACE_ID", 'verifyEmail': 'EMAIL', 'nickName': 'NICKNAME', 'password': 'PASSWORD', 'username': 'EMAIL', 'email': 'EMAIL', 'deviceName': 'NAME' }) class YAMLWriter: """Read and Write API request data to YAML files. Arguments --------- module : str name of module that is being tested dev_type : str device type being tested Attributes ---------- self.file_path : Path Path to YAML directory, default to API dir in tests folder self.file : Path Path to YAML file based on device type and module self.existings_yaml : dict Existing YAML data read to dict object self._existing_api : dict, optional Existing data dict of a specific API call Methods ------- self.existing_api() Return existing data dict of a specific API call or None self.write_api(api, data, overwrite=False) Writes data to YAML file for a specific API call, set overwrite=True to overwrite existing data """ def __init__(self, module, dev_type): """Init the YAMLWriter class. Arguments ---------- module : str name of module that is being tested dev_type : str device type being tested """ self.file_path = self._get_path(module) self.file = Path.joinpath(self.file_path, dev_type + '.yaml') self.new_file = self._new_file() self.existing_yaml = self._get_existing_yaml() self._existing_api = None @staticmethod def _get_path(module) -> Path: yaml_dir = Path.joinpath(Path(__file__).parent, 'api', module) if not yaml_dir.exists(): yaml_dir.mkdir(parents=True) return yaml_dir def _new_file(self) -> bool: if not self.file.exists(): logger.debug(f'Creating new file {self.file}') self.file.touch() return True return False def _get_existing_yaml(self) -> Any: if self.new_file: return None with open(self.file, 'rb') as f: data = yaml.full_load(f) return data def existing_api(self, method) -> bool: """Check YAML file for existing data for API call. Arguments ---------- method : str Name of method being tested Returns ------- dict or None Existing data for API call or None """ if self.existing_yaml is not None: current_dict = self.existing_yaml.get(method) self._existing_api = current_dict if current_dict is not None: logger.debug(f'API call {method} already exists in {self.file}') return True return False def write_api(self, method, yaml_dict, overwrite=False): """Write API data to YAML file. Arguments ---------- method : str Name of method being tested yaml_dict : dict Data to write to YAML file overwrite : bool, optional Overwrite existing data, default to False """ if self.existing_yaml is not None: current_dict = self.existing_yaml.get(method) if current_dict is not None and overwrite is False: logger.debug(f'API call {method} already exists in {self.file}') return self.existing_yaml[method] = yaml_dict else: self.existing_yaml = {method: yaml_dict} with open(self.file, 'w', encoding='utf-8') as f: yaml.dump(self.existing_yaml, f, encoding='utf-8') def api_scrub(api_dict, device_type=None): """Recursive function to scrub all sensitive data from API call. Arguments ---------- api_dict : dict API call data to scrub device_type : str, optional Device type to use for default values Returns ------- dict Scrubbed API call data """ def id_cleaner(key, value): if key.upper() in ID_KEYS: return f"{device_type or 'UNKNOWN'}-{key.upper()}" if key in API_DEFAULTS: return API_DEFAULTS[key] return value def nested_dict_iter(nested, mapper, last_key=None): if isinstance(nested, dict): if nested.get('deviceType') is not None: nonlocal device_type device_type = nested['deviceType'] out = {} for key, value in nested.items(): out[key] = nested_dict_iter(value, mapper, key) return out if isinstance(nested, list): return [nested_dict_iter(el, mapper, last_key) for el in nested] if not last_key: return nested return mapper(last_key, nested) return nested_dict_iter(api_dict, id_cleaner) class TestBase: """Base class for all tests. Contains instantiated VeSync object and mocked API call for call_api() function. Attributes ---------- self.mock_api : Mock Mock for call_api() function self.manager : VeSync Instantiated VeSync object that is logged in self.caplog : LogCaptureFixture Pytest fixture for capturing logs """ overwrite = False write_api = False @pytest.fixture(autouse=True, scope='function') def setup(self, caplog): """Fixture to instantiate VeSync object, start logging and start Mock. Attributes ---------- self.mock_api : Mock self.manager : VeSync self.caplog : LogCaptureFixture Yields ------ Class instance with mocked call_api() function and VeSync object """ self.mock_api_call = patch('pyvesync.helpers.Helpers.call_api') self.caplog = caplog self.mock_api = self.mock_api_call.start() self.mock_api.create_autospect() self.mock_api.return_value.ok = True self.manager = VeSync('EMAIL', 'PASSWORD', debug=True) self.manager.enabled = True self.manager.token = Defaults.token self.manager.account_id = Defaults.account_id caplog.set_level(logging.DEBUG) yield self.mock_api_call.stop() def assert_test(test_func, all_kwargs, dev_type=None, write_api=False, overwrite=False): """Test pyvesync API calls against existing API. Set `write_api=True` to True to write API call data to YAML file. This will not overwrite existing data unless overwrite is True. The overwrite argument is only used when API changes, defaults to false for development testing. `overwrite=True` and `write_api=True` need to both be set to overwrite existing data. Arguments ---------- test_func : method Method that is being tested all_kwargs : dict Dictionary of call_api() arguments dev_type : str, optional Device type being tested write_api : bool, optional Write API call data to YAML file, default to False overwrite : bool, optional Overwrite existing data ONLY USE FOR CHANGING API, default to False. Must be set with `write_api=True` Returns ------- bool True if test passes, False if test fails Asserts ------- Asserts that the API call data matches the expected data """ if all_kwargs.get('json_object') is not None: all_kwargs['json_object'] = api_scrub(all_kwargs['json_object'], dev_type) if all_kwargs.get('headers') is not None: all_kwargs['headers'] = api_scrub(all_kwargs['headers'], dev_type) mod = test_func.__module__.split(".")[-1] if dev_type is None: cls_name = test_func.__self__.__class__.__name__ else: cls_name = dev_type method_name = test_func.__name__ writer = YAMLWriter(mod, cls_name) if overwrite is True and write_api is True: writer.write_api(method_name, all_kwargs, overwrite) return True if writer.existing_api(method_name) is False: logger.debug("No existing, API data for %s %s %s", mod, cls_name, method_name) if write_api is True: logger.debug("Writing API data for %s %s %s", mod, cls_name, method_name) writer.write_api(method_name, all_kwargs, overwrite) else: logger.debug("Not writing API data for %s %s %s", mod, cls_name, method_name) assert writer._existing_api == all_kwargs return True pyvesync-2.1.18/tox.ini000066400000000000000000000010121475441503300147760ustar00rootroot00000000000000[tox] envlist = py311, py310, py39, pylint, lint, mypy skip_missing_interpreters = True ignore_basepython_conflict = True [testenv] deps = pytest pyyaml commands = pytest -q {posargs} [testenv:pylint] ignore_errors = True deps = pylint commands = pylint {posargs} src/pyvesync [testenv:lint] ignore_errors = True deps = flake8 flake8-docstrings commands = flake8 src/pyvesync [testenv:mypy] ignore_errors = True deps = mypy types-requests allowlist_externals = mypy commands = mypy src/pyvesync