pax_global_header00006660000000000000000000000064146401205130014506gustar00rootroot0000000000000052 comment=d437c42633a89f9bc687d272404a472f624aca2e webdjoe-pyvesync-d437c42/000077500000000000000000000000001464012051300153375ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/.coveragerc000066400000000000000000000013201464012051300174540ustar00rootroot00000000000000# .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_reportwebdjoe-pyvesync-d437c42/.gitignore000066400000000000000000000005271464012051300173330ustar00rootroot00000000000000*.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 .venvwebdjoe-pyvesync-d437c42/.pylintrc000066400000000000000000000030641464012051300172070ustar00rootroot00000000000000[MASTER] ignore=src/tests, tools/ [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, 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-public-methods, too-many-return-statements, too-many-statements, 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=90webdjoe-pyvesync-d437c42/CONTRIBUTING.md000066400000000000000000000055541464012051300176010ustar00rootroot00000000000000# 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](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 ``` webdjoe-pyvesync-d437c42/LICENSE000066400000000000000000000020541464012051300163450ustar00rootroot00000000000000MIT 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. webdjoe-pyvesync-d437c42/MANIFEST.in000077500000000000000000000001021464012051300170710ustar00rootroot00000000000000include README.md LICENSE requirements.txt exclude test prune testwebdjoe-pyvesync-d437c42/README.md000066400000000000000000001035111464012051300166170ustar00rootroot00000000000000# 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. Multicolor Bulb (XYD0001) ### Levoit Humidifiers 1. Dual 200S 2. Classic 300S 3. LUH-D301S-WEU Dual (200S) 4. LV600S 5. OasisMist LUS-O415S-WUS 6. OasisMist LUH-M101S-WUS Cosori Air Fryer 1. Cosori 3.7 and 5.8 Quart Air Fryer ## 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). webdjoe-pyvesync-d437c42/azure-pipelines.yml000066400000000000000000000031171464012051300212000ustar00rootroot00000000000000# 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' webdjoe-pyvesync-d437c42/mypy.ini000066400000000000000000000004711464012051300170400ustar00rootroot00000000000000[mypy] python_version=3.8 [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 = Truewebdjoe-pyvesync-d437c42/requirements.txt000066400000000000000000000000201464012051300206130ustar00rootroot00000000000000requests>=2.20.0webdjoe-pyvesync-d437c42/setup.cfg000077500000000000000000000004251464012051300171640ustar00rootroot00000000000000[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 [pycodestyle] max-line-length = 90 statistics = Truewebdjoe-pyvesync-d437c42/setup.py000066400000000000000000000024211464012051300170500ustar00rootroot00000000000000"""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.12', 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.8', ], 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', 'yaml', 'tox'] }, python_requires='>=3.9', ) webdjoe-pyvesync-d437c42/src/000077500000000000000000000000001464012051300161265ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/pyvesync/000077500000000000000000000000001464012051300200065ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/pyvesync/__init__.py000077500000000000000000000003151464012051300221210ustar00rootroot00000000000000"""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' ) webdjoe-pyvesync-d437c42/src/pyvesync/helpers.py000066400000000000000000000457571464012051300220440ustar00rootroot00000000000000"""Helper functions for VeSync API.""" import hashlib import logging import time import json import colorsys from dataclasses import dataclass, field, InitVar from typing import Any, Dict, NamedTuple, Optional, Union import re import requests 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" NUMERIC = Optional[Union[int, float, str]] class Helpers: """VeSync Helper Functions.""" @staticmethod def req_headers(manager) -> Dict[str, str]: """Build header for api requests.""" headers = { 'accept-language': 'en', 'accountId': manager.account_id, 'appVersion': APP_VERSION, 'content-type': 'application/json', 'tk': manager.token, 'tz': manager.time_zone, } return headers @staticmethod def req_header_bypass() -> Dict[str, str]: """Build header for api requests on 'bypass' endpoint.""" return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': 'okhttp/3.12.1', } @staticmethod def req_body_base(manager) -> Dict[str, str]: """Return universal keys for body of api requests.""" return {'timeZone': manager.time_zone, 'acceptLanguage': 'en'} @staticmethod def req_body_auth(manager) -> Dict[str, str]: """Keys for authenticating api requests.""" return {'accountID': manager.account_id, 'token': manager.token} @staticmethod def req_body_details() -> Dict[str, str]: """Detail keys for api requests.""" return { 'appVersion': APP_VERSION, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'traceId': str(int(time.time())), } @classmethod def req_body(cls, manager, type_) -> Dict[str, Any]: """Builder for body of api requests.""" body = cls.req_body_base(manager) if type_ == 'login': body |= cls.req_body_details() # type: ignore body |= { 'email': manager.username, 'password': cls.hash_password(manager.password), 'devToken': '', 'userType': USER_TYPE, 'method': 'login' } # type: ignore return body body |= cls.req_body_auth(manager) # type: ignore if type_ == 'devicestatus': return body body |= cls.req_body_details() # type: ignore 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) -> float: """Credit for conversion to itsnotlupus/vesync_wsproxy.""" hex_conv = hex_string.split(':') converted_hex = (int(hex_conv[0], 16) + int(hex_conv[1], 16)) / 8192 return converted_hex @staticmethod def hash_password(string) -> str: """Encode password.""" return hashlib.md5(string.encode('utf-8')).hexdigest() shouldredact = True @classmethod def redactor(cls, stringvalue: str) -> str: """Redact sensitive strings from debug output.""" if cls.shouldredact: stringvalue = re.sub(r''.join(( '(?i)', '((?<=token": ")|', '(?<=password": ")|', '(?<=email": ")|', '(?<=tk": ")|', '(?<=accountId": ")|', '(?<=authKey": ")|', '(?<=uuid": ")|', '(?<=cid": "))', '[^"]+') ), '##_REDACTED_##', stringvalue) return stringvalue @staticmethod def nested_code_check(response: dict) -> bool: """Return true if all code values are 0.""" if isinstance(response, dict): for key, value in response.items(): if key == 'code': if value != 0: return False elif isinstance(value, dict): if not Helpers.nested_code_check(value): return False return True @staticmethod def call_api(api: str, method: str, json_object: Optional[dict] = None, headers: Optional[dict] = None) -> tuple: """Make API calls by passing endpoint, header and body.""" 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: # pylint: disable=broad-except 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 if isinstance(r, dict) and r.get('code') == 0: return True return False @staticmethod def build_details_dict(r: dict) -> dict: """Build details dictionary from API response.""" return { 'active_time': r.get('activeTime', 0), 'energy': r.get('energy', 0), 'night_light_status': r.get('nightLightStatus', None), 'night_light_brightness': r.get('nightLightBrightness', None), 'night_light_automode': r.get('nightLightAutomode', None), 'power': r.get('power', 0), 'voltage': r.get('voltage', 0), } @staticmethod def build_energy_dict(r: dict) -> dict: """Build energy dictionary from API response.""" 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.""" 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): """Build body dict for bypass calls.""" bdy = {} bdy.update( **cls.req_body(manager, "bypass") ) bdy['method'] = 'bypassV2' bdy['debugMode'] = False bdy['deviceRegion'] = DEFAULT_REGION return bdy @staticmethod def bypass_header(): """Build bypass header dict.""" 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.""" tuple_str = '' for key, val in named_tuple._asdict().items(): tuple_str += f'{key}: {val}, ' return tuple_str class HSV(NamedTuple): """HSV color space.""" hue: float saturation: float value: float class RGB(NamedTuple): """RGB color space.""" 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. To instantiate pass kw arguments for colors hue, saturation and value or red, green and blue. Instance attributes are: hsv (nameduple) : hue (0-360), saturation (0-100), value (0-100) rgb (namedtuple) : red (0-255), green (0-255), blue """ 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, green, blue, hue, saturation, value): """Check HSV or RGB Values and create named tuples.""" if any(x is not None for x in [hue, saturation, value]): self.hsv = HSV(*self.valid_hsv(hue, saturation, value)) self.rgb = self.hsv_to_rgb(hue, saturation, value) elif any(x is not None for x in [red, green, blue]): self.rgb = RGB(*self.valid_rgb(red, green, blue)) self.hsv = self.rgb_to_hsv(red, green, blue) else: logger.error('No color values provided') @staticmethod def min_max(value: Union[int, 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: Union[int, float, str], s: Union[int, float, str], v: Union[int, 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, saturation, value) -> 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, green, blue) -> 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 for timers. Parameters ---------- 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 Attributes ---------- update_time : int Timestamp of last update Properties ---------- 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 Methods ------- start() Restarts paused timer end() Ends timer pause() Pauses timer update(time_remaining: Optional[int] = None, status: Optional[str] = None) Updates timer with new time remaining and/or status """ timer_duration: int action: str id: int = 1 remaining: InitVar[Optional[int]] = None _status: str = 'active' _remain: int = 0 update_time: Optional[int] = int(time.time()) def __post_init__(self, remaining) -> 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']: raise ValueError(f'Invalid status {status}') self._internal_update() if status == 'done' or self._status == 'done': return self.end() 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: return self.end() 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.""" if self.time_remaining > 0 and self.status == 'active': return True return False @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: Optional[int] = None, status: Optional[str] = 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 Returns ------- None """ 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 webdjoe-pyvesync-d437c42/src/pyvesync/vesync.py000066400000000000000000000362051464012051300216750ustar00rootroot00000000000000"""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.""" 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 """ 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.""" 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.""" 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 ------- bool 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.""" if ( self.last_update_ts is None or (time.time() - self.last_update_ts) > self.update_interval ): return True return False 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. Returns ------- None """ 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 about 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.""" devices = list(self._dev_list.keys()) for dev in chain(*devices): dev.get_details() webdjoe-pyvesync-d437c42/src/pyvesync/vesyncbasedevice.py000066400000000000000000000126521464012051300237100ustar00rootroot00000000000000"""Base class for all VeSync devices.""" import logging import json from typing import Optional, Union from pyvesync.helpers import Helpers as helper logger = logging.getLogger(__name__) class VeSyncBaseDevice: """Properties shared across all VeSync devices.""" def __init__(self, details: dict, manager): """Initialize VeSync device base class.""" self.manager = manager if 'cid' in details and details['cid'] is not None: self.device_name: str = details.get('deviceName', None) self.device_image: Optional[str] = details.get('deviceImg', None) self.cid: str = details.get('cid', None) self.connection_status: str = details.get('connectionStatus', None) self.connection_type: Optional[str] = details.get( 'connectionType', None) self.device_type: str = details.get('deviceType', None) self.type: str = details.get('type', None) self.uuid: Optional[str] = details.get('uuid', None) self.config_module: str = details.get( 'configModule', None) self.mac_id: Optional[str] = details.get('macID', None) self.mode: Optional[str] = details.get('mode', None) self.speed: Union[str, int, None] = details.get('speed', None) self.extension = details.get('extension', None) self.current_firm_version = details.get( 'currentFirmVersion', None) self.device_region: Optional[str] = details.get('deviceRegion', None) 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 = 'off' else: self.device_status = details.get('deviceStatus', None) else: logger.error('No cid found for %s', self.__class__.__name__) def __eq__(self, other): """Use device CID and subdevice number to test equality.""" return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no) def __hash__(self): """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): """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): """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.""" if self.device_status == 'on': return True return False @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.""" 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.""" 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) webdjoe-pyvesync-d437c42/src/pyvesync/vesyncbulb.py000066400000000000000000001256121464012051300225430ustar00rootroot00000000000000"""Etekcity/Valceno Smart Light Bulbs.""" import logging import json from typing import Union, Dict, Optional, NamedTuple from abc import ABCMeta, abstractmethod from pyvesync.helpers import Helpers as helpers, Color from pyvesync.vesyncbasedevice import VeSyncBaseDevice logger = logging.getLogger(__name__) NUMERIC_T = Optional[Union[int, float, str]] # Possible features - dimmable, color_temp, rgb_shift 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' } } bulb_modules: dict = {k: v['module'] for k, v in feature_dict.items()} __all__: list = list(bulb_modules.values()) + ['bulb_modules'] def pct_to_kelvin(pct: float, max_k: int = 6500, min_k: int = 2700) -> float: """Convert percent to kelvin.""" kelvin = ((max_k - min_k) * pct / 100) + min_k return kelvin class VeSyncBulb(VeSyncBaseDevice): """Base class for VeSync Bulbs.""" __metaclass__ = ABCMeta def __init__(self, details: Dict[str, Union[str, list]], manager): """Initialize VeSync smart bulb base class.""" super().__init__(details, manager) self._brightness = int(0) self._color_temp = int(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: Optional[Color] = None self.features: Optional[list] = 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(f"No configuration set for {self.device_name}") self._rgb_values = { 'red': 0, 'green': 0, 'blue': 0 } # self.get_config() # self.current_firm_version = self.config.get('currentFirmVersion', None) # self.latest_firm_version = self.config.get('latestFirmVersion', None) # self.firmware_url = self.config.get('firmwareUrl', None) @property def brightness(self) -> int: """Return brightness of vesync bulb.""" 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.""" 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).""" 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 of bulb. (from 0 to 360).""" 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 of bulb in percent (0-100).""" 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 of bulb in percent (0-100).""" if self.rgb_shift_feature and self._color is not None: return self._color.hsv.value return 0 @property def color(self) -> Optional[Color]: """Return color of bulb in the form of a dataclass with two attributes. self.color.hsv -> (NamedTuple) Hue: float 0-360, Saturation: float 0-100 and Value: float 0-100 self.color.rgb -> (NamedTuple) Red: float 0-255, Green: float 0-255 and Blue: float 0-255 """ if self.rgb_shift_feature is True and self._color is not None: return self._color return None @color.setter def color(self, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, hue: Optional[float] = None, saturation: Optional[float] = None, value: Optional[float] = None) -> None: self._color = Color(red=red, green=green, blue=blue, hue=hue, saturation=saturation, value=value) @property def color_hsv(self) -> Optional[NamedTuple]: """Return color of bulb in hsv.""" if self.rgb_shift_feature is True and self._color is not None: return self._color.hsv return None @property def color_rgb(self) -> Optional[NamedTuple]: """Return color of bulb in rgb.""" if self.rgb_shift_feature is True and self._color is not None: return self._color.rgb return None @property def color_mode(self) -> Optional[str]: """Return color mode of bulb.""" # { white, hsv } 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.""" if self.features is not None and 'dimmable' in self.features: return True return False @property def color_temp_feature(self) -> bool: """Return true if bulb supports white color temperature changes.""" if self.features is not None and 'color_temp' in self.features: return True return False @property def rgb_shift_feature(self) -> bool: """Return True if bulb supports changing color (RGB).""" if self.features is not None and 'rgb_shift' in self.features: return True return False 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: Optional[NUMERIC_T] = None, saturation: Optional[NUMERIC_T] = None, value: Optional[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: if 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) else: if 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) else: if 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: Union[int, 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): if self._brightness is not None: brightness_update = self.brightness else: brightness_update = 100 return brightness_update def _validate_color_temp(self, temp: int, start: int = 0, stop: int = 100): """Validate color temperature.""" try: temp_update = max(start, (min(stop, int( round(float(temp), 0))))) except (ValueError, TypeError): if self._color_temp is not None: temp_update = self._color_temp else: temp_update = 100 return temp_update @staticmethod def _validate_any(value: Union[int, float, str], start: Union[int, float] = 0, stop: Union[int, float] = 100, default: Union[int, float] = 100) -> float: """Validate any value.""" try: value_update = max(float(start), (min(float(stop), round(float(value), 2)))) except (ValueError, TypeError): value_update = default return value_update @abstractmethod def set_status(self) -> bool: """Set vesync bulb attributes(brightness, color_temp, etc).""" @abstractmethod def get_details(self) -> None: """Get vesync bulb details.""" @abstractmethod def _interpret_apicall_result(self, response) -> None: """Update bulb status from any api call response.""" @abstractmethod def toggle(self, status: str) -> bool: """Toggle vesync lightbulb.""" @abstractmethod def get_config(self) -> None: """Call api to get configuration details and firmware.""" pass def set_hsv(self, hue, saturation, value): """Set HSV if supported by bulb. Hue 0-360, Saturation 0-100, Value 0-100. """ if self.rgb_shift_feature is False: logger.debug("HSV not supported by bulb") return False def set_rgb(self, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None) -> bool: """Set RGB if supported by bulb. Red 0-255, Green 0-255, Blue 0-255.""" if self.rgb_shift_feature is False: logger.debug("RGB not supported by bulb") return False return True def turn_on(self) -> bool: """Turn on vesync bulbs.""" 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.""" 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.""" 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.""" 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) -> Optional[NamedTuple]: """Legacy Method .... Depreciated.""" if self._color is not None: return self._color.rgb return None @property def color_value_hsv(self) -> Optional[NamedTuple]: """Legacy Method .... Depreciated.""" if self._color is not None: return self._color.hsv return None class VeSyncBulbESL100MC(VeSyncBulb): """Etekcity ESL100 Multi Color Bulb.""" def __init__(self, details: Dict[str, Union[str, list]], manager): """Instantiate ESL100MC Multicolor Bulb.""" super().__init__(details, manager) self.details: dict = {} def get_details(self) -> None: """Get ESL100MC Details.""" 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): """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)) return True def set_brightness(self, brightness: int) -> bool: """Set brightness of bulb.""" return self.set_status(brightness=brightness) def set_rgb_color(self, red: float, green: float, blue: float) -> bool: """Set RGB Color of bulb.""" return self.set_status(red=red, green=green, blue=blue) def set_rgb(self, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None) -> bool: """Set RGB Color of bulb.""" return self.set_status(red=red, green=green, blue=blue) def set_hsv(self, hue, saturation, value): """Set HSV Color of bulb.""" 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.""" return self.set_status(brightness=100) def set_status(self, brightness: Optional[NUMERIC_T] = None, red: Optional[NUMERIC_T] = None, green: Optional[NUMERIC_T] = None, blue: Optional[NUMERIC_T] = None) -> bool: """Set status of VeSync ESL100MC.""" 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: """Toggle bulb status.""" 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.""" def __init__(self, details: dict, manager) -> None: """Initialize Etekcity ESL100 Dimmable Bulb.""" super().__init__(details, manager) self.details: dict = {} def get_details(self) -> None: """Get details of dimmable bulb.""" 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: """Get configuration of dimmable bulb.""" 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) -> bool: """Toggle dimmable bulb.""" 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.""" 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, manager): """Initialize Etekcity Tunable white bulb.""" super().__init__(details, manager) def get_details(self) -> None: """Get details of tunable bulb.""" 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) -> 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: """Get configuration and firmware info of tunable bulb.""" 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) -> bool: """Toggle tunable bulb.""" 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, manager): """Initialize Multicolor bulb.""" super().__init__(details, manager) def get_details(self) -> None: """Get details of multicolor bulb.""" 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) -> 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: """Get configuration and firmware info of multicolor bulb.""" 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: """Toggle multicolor bulb.""" 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: """Set RGB - red, green & blue 0-255.""" 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: """Set HSV Values.""" arg_dict = {"hue": hue, "saturation": saturation, "value": value} if hue is not None: hue_update: NUMERIC_T = self._validate_any(hue, 0, 360, 360) else: hue_update = "" if saturation is not None: sat_update: NUMERIC_T = self._validate_any(saturation, 0, 100, 100) else: sat_update = "" if value is not None: value_update: NUMERIC_T = self._validate_any(value, 0, 100, 100) else: value_update = "" arg_dict = { "hue": hue_update, "saturation": sat_update, "brightness": value_update } # 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} same_colors = True for key, val in arg_dict.items(): if val != "": if val != current_dict[key]: same_colors = False if self.device_status == 'on' and same_colors: logger.debug("Device already in requested state") return True for key, val in arg_dict.items(): if key == 'hue' and isinstance(val, float): arg_dict[key] = int(round(val*27.77778, 0)) if key == "saturation" and isinstance(val, float): arg_dict[key] = int(round(val*100, 0)) if key == "brightness" and isinstance(val, float): arg_dict[key] = int(round(val, 0)) 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, brightness: NUMERIC_T = None, color_temp: NUMERIC_T = None, color_saturation: NUMERIC_T = None, color_hue: NUMERIC_T = None, color_mode: Optional[str] = None, color_value: NUMERIC_T = None ) -> bool: """Set multicolor bulb parameters. No arguments turns bulb on. Parameters ---------- brightness : int, optional brightness between 0 and 100, by default None color_temp : int, optional color temperature between 0 and 100, by default None color_mode : str, optional color mode hsv or white, by default None color_hue : float, optional color hue between 0 and 360, by default None color_saturation : float, optional color saturation between 0 and 100, by default None color_value : float, optional color value between 0 and 100, by default None 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 color_hue is not None or color_value is not None or \ color_saturation is not None: 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 all(locals().get(k) is None for k in force_list): # 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 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: valid_color_temp = self._validate_any(color_temp, 0, 100, 100) if valid_color_temp is not None: request_dict['colorTemp'] = int(valid_color_temp) request_dict['colorMode'] = 'white' # """Set Color Mode of Bulb (white / hsv).""" if color_mode is not None: if not isinstance(color_mode, str): logger.error('Error: color_mode should be a string value') return False color_mode = color_mode.lower() possible_modes = {'white': 'white', 'color': 'hsv', 'hsv': 'hsv'} if color_mode not in possible_modes: logger.error( 'Color mode specified is not acceptable ' '(Try: "white"/"color"/"hsv")') return False 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 webdjoe-pyvesync-d437c42/src/pyvesync/vesyncfan.py000066400000000000000000003000071464012051300223540ustar00rootroot00000000000000"""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'], '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] }, 'OASISMIST': { 'module': 'VeSyncHumid200300S', 'models': ['LUH-O451S-WUS', 'LUH-O451S-WUSR', 'LUH-O451S-WEU'], 'features': ['warm_mist'], 'mist_modes': ['humidity', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)), 'warm_mist_levels': list(range(4)) }, 'OASISMIST1000S': { 'module': 'VeSyncHumid1000S', 'models': ['LUH-M101S-WUS'], 'features': [], 'mist_modes': ['auto', 'sleep', 'manual'], 'mist_levels': list(range(1, 10)) }, 'Superior6000S': { 'module': 'VeSyncSuperior6000S', 'models': ['LEH-S601S-WUS'], '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'], 'features': ['air_quality'] }, '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)) } } logger = logging.getLogger(__name__) def model_dict() -> dict: """Build purifier and humidifier model dictionary.""" 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.""" 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): """Base class for Levoit Purifier Bypass API Calls.""" def __init__(self, details: Dict[str, list], manager): """Initialize air purifier devices. Instantiated by VeSync manager object. Inherits from VeSyncBaseDevice class. Arguments ---------- 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 `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: >>> { >>> '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 >>> } See Also -------- VeSyncBaseDevice : Parent class for all VeSync devices """ 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. standard modes are: ['getPurifierStatus', 'setSwitch', 'setNightLight', 'setLevel', 'setPurifierMode', 'setDisplay', 'setChildLock'] """ 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.""" 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'] = dev_dict.get('display', False) 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.""" 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. Arguments ---------- None Returns ------- Timer or None 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 -------- Timer : Timer object used to track device timers """ 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. Arguments ---------- timer_duration: int Duration of timer in seconds Returns ------- bool """ 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 """ 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.""" 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.""" 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. Arguments ---------- mode: bool True to turn child lock on, False to turn off Returns ------- bool """ 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%.""" 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. Arguments ---------- mode: str Mode to set purifier. Based on device modes in attribute `modes` Returns ------- bool """ 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.""" 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.""" 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.""" 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.""" 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.""" return self.toggle_switch(True) def turn_off(self): """Turn Bypass Purifier off.""" return self.toggle_switch(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 purifier display - %s", self.device_name) return False def turn_on_display(self) -> bool: """Turn Display on.""" return self.set_display(True) def turn_off_display(self): """Turn Display off.""" return self.set_display(False) def set_night_light(self, mode: str) -> bool: """Set night list - on, off or dim.""" 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 (1-3).""" 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.""" return bool(self.details['display']) @property def screen_status(self) -> bool: """Get display status.""" return bool(self.details['display']) @property def child_lock(self) -> bool: """Get child lock state.""" return bool(self.details['child_lock']) @property def night_light(self) -> str: """Get night light state (on/dim/off).""" return str(self.details['night_light']) def display(self) -> None: """Return formatted device info to stdout.""" 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: """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': 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.""" 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'] 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.""" 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, optional Action to perform, on or off, by default 'off' method : str, optional Method to use, by default 'powerSwitch' - TODO: Implement other methods Returns ------- bool """ 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, optional Preference for auto mode, by default 'default' options are: default, efficient, quiet room_size : int, optional 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. Parameters ---------- speed : int, optional Speed to set based on levels in configuration dict, by default None If None, will cycle through 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 """ 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.lower() } 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.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 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 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.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) 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 humidity') 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) 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'] @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): """True if fan will stay on to dry the filters when humidifier is off, \ False otherwise.""" 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 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 webdjoe-pyvesync-d437c42/src/pyvesync/vesynckitchen.py000066400000000000000000000561161464012051300232460ustar00rootroot00000000000000"""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): """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) webdjoe-pyvesync-d437c42/src/pyvesync/vesyncoutlet.py000066400000000000000000000610671464012051300231360ustar00rootroot00000000000000"""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'}, } 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 outdoor outlet on and return True if successful.""" return bool(self.toggle('on')) def turn_off(self) -> bool: """Turn outdoor outlet off and return True if successful.""" return bool(self.toggle('off')) webdjoe-pyvesync-d437c42/src/pyvesync/vesyncswitch.py000066400000000000000000000305441464012051300231170ustar00rootroot00000000000000"""Classes for VeSync Switch Devices.""" 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__) feature_dict: Dict[str, Dict[str, Union[list, str]]] = { 'ESWL01': { 'module': 'VeSyncWallSwitch', 'features': [] }, 'ESWD16': { 'module': 'VeSyncDimmerSwitch', 'features': ['dimmable'] }, 'ESWL03': { 'module': 'VeSyncWallSwitch', 'features': [] } } 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.""" __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) webdjoe-pyvesync-d437c42/src/tests/000077500000000000000000000000001464012051300172705ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/README.md000066400000000000000000000306021464012051300205500ustar00rootroot00000000000000# 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. webdjoe-pyvesync-d437c42/src/tests/api/000077500000000000000000000000001464012051300200415ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesync/000077500000000000000000000000001464012051300213505ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesync/VeSync.yaml000066400000000000000000000013211464012051300234400ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncbulb/000077500000000000000000000000001464012051300222155ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesyncbulb/ESL100.yaml000066400000000000000000000032151464012051300237460ustar00rootroot00000000000000set_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncbulb/ESL100CW.yaml000066400000000000000000000052261464012051300242040ustar00rootroot00000000000000set_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncbulb/ESL100MC.yaml000066400000000000000000000073041464012051300241710ustar00rootroot00000000000000enable_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncbulb/XYD0001.yaml000066400000000000000000000104061464012051300240470ustar00rootroot00000000000000enable_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/000077500000000000000000000000001464012051300220355ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Classic200S.yaml000066400000000000000000000144741464012051300246610ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Classic300S.yaml000066400000000000000000000144021464012051300246510ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Core200S.yaml000066400000000000000000000132251464012051300241610ustar00rootroot00000000000000change_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Core300S.yaml000066400000000000000000000143171464012051300241650ustar00rootroot00000000000000auto_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Core400S.yaml000066400000000000000000000143171464012051300241660ustar00rootroot00000000000000auto_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Core600S.yaml000066400000000000000000000143171464012051300241700ustar00rootroot00000000000000auto_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/Dual200S.yaml000066400000000000000000000143411464012051300241560ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LAP-EL551S-AUS.yaml000066400000000000000000000075661464012051300246550ustar00rootroot00000000000000change_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/bypassV2webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LAP-V102S-AASR.yaml000066400000000000000000000075671464012051300246510ustar00rootroot00000000000000change_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LAP-V201S-AASR.yaml000066400000000000000000000075671464012051300246510ustar00rootroot00000000000000change_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LEH-S601S-WUS.yaml000066400000000000000000000156421464012051300245670ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LUH-A602S-WUSR.yaml000066400000000000000000000156041464012051300247060ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LUH-M101S-WUS.yaml000066400000000000000000000145201464012051300245660ustar00rootroot00000000000000automatic_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LUH-O451S-WUS.yaml000066400000000000000000000144341464012051300246040ustar00rootroot00000000000000automatic_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: 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-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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncfan/LV-PUR131S.yaml000066400000000000000000000046521464012051300242650ustar00rootroot00000000000000change_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/000077500000000000000000000000001464012051300226055ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/ESO15-TB.yaml000066400000000000000000000055301464012051300245730ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/ESW01-EU.yaml000066400000000000000000000053521464012051300246040ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/ESW03-USA.yaml000066400000000000000000000053601464012051300247240ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/ESW15-USA.yaml000066400000000000000000000067731464012051300247400ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncoutlet/wifi-switch-1.3.yaml000066400000000000000000000026401464012051300262270ustar00rootroot00000000000000get_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncswitch/000077500000000000000000000000001464012051300225725ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/api/vesyncswitch/ESWD16.yaml000066400000000000000000000054651464012051300244010ustar00rootroot00000000000000indicator_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncswitch/ESWL01.yaml000066400000000000000000000023741464012051300243770ustar00rootroot00000000000000turn_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 webdjoe-pyvesync-d437c42/src/tests/api/vesyncswitch/ESWL03.yaml000066400000000000000000000023741464012051300244010ustar00rootroot00000000000000turn_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 webdjoe-pyvesync-d437c42/src/tests/call_json.py000066400000000000000000000451011464012051300216070ustar00rootroot00000000000000import 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 webdjoe-pyvesync-d437c42/src/tests/call_json_bulbs.py000066400000000000000000000142221464012051300227760ustar00rootroot00000000000000""" 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) webdjoe-pyvesync-d437c42/src/tests/call_json_fans.py000066400000000000000000000306721464012051300226250ustar00rootroot00000000000000""" 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_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-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, } 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") webdjoe-pyvesync-d437c42/src/tests/call_json_outlets.py000066400000000000000000000113331464012051300233660ustar00rootroot00000000000000""" 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 utils import FunctionResponses, Defaults from pyvesync import vesyncoutlet OUTLETS = vesyncoutlet.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, ) 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, } 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 webdjoe-pyvesync-d437c42/src/tests/call_json_switches.py000066400000000000000000000042341464012051300235220ustar00rootroot00000000000000""" 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} webdjoe-pyvesync-d437c42/src/tests/conftest.py000066400000000000000000000046611464012051300214760ustar00rootroot00000000000000def 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) returnwebdjoe-pyvesync-d437c42/src/tests/request_api.yaml000066400000000000000000000000001464012051300224630ustar00rootroot00000000000000webdjoe-pyvesync-d437c42/src/tests/test_all_devices.py000066400000000000000000000066271464012051300231660ustar00rootroot00000000000000""" 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 webdjoe-pyvesync-d437c42/src/tests/test_bulbs.py000066400000000000000000000231231464012051300220110ustar00rootroot00000000000000""" 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 webdjoe-pyvesync-d437c42/src/tests/test_fans.py000066400000000000000000000357361464012051300216460ustar00rootroot00000000000000""" 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 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 - 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) webdjoe-pyvesync-d437c42/src/tests/test_outlets.py000066400000000000000000000233711464012051300224060ustar00rootroot00000000000000""" 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 = [] [OUTLET_PARAMS.append([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'], ['get_yearly_energy'], ['get_monthly_energy'], ['get_weekly_energy']] device_methods = { 'ESW15-USA': [['turn_on_nightlight'], ['turn_off_nightlight']], } 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` """ # Set return value for call_api based on call_json_bulb.DETAILS_RESPONSES self.mock_api.return_value = call_json_outlets.DETAILS_RESPONSES[dev_type] # Instantiate device from device list return item device_config = call_json.DeviceList.device_list_item(dev_type) _, outlet_obj = object_factory(dev_type, device_config, self.manager) # Get and run method from object method_call = getattr(outlet_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 attributes match default values assert int(outlet_obj.details['active_time']) == int(Defaults.active_time) assert int(outlet_obj.details['energy']) == int( call_json_outlets.OutletDefaults.energy) assert int(outlet_obj.details['power']) == int( call_json_outlets.OutletDefaults.power) assert int(outlet_obj.details['voltage']) == int( call_json_outlets.OutletDefaults.voltage) 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 call_json_bulbs.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 from call_json.DeviceList.device_list_item() device_config = call_json.DeviceList.device_list_item(dev_type) # Instantiate device from device list return item _, outlet_obj = object_factory(dev_type, device_config, self.manager) # Ensure method runs based on device configuration if method[0] == 'turn_on': outlet_obj.device_status == 'off' if method[0] == 'turn_off': outlet_obj.device_status == 'on' # Get method from device object method_call = getattr(outlet_obj, method[0]) # Call method with kwargs if present 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) # 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', OUTLET_DEV_TYPES) 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 webdjoe-pyvesync-d437c42/src/tests/test_switches.py000066400000000000000000000210141464012051300225300ustar00rootroot00000000000000""" 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 webdjoe-pyvesync-d437c42/src/tests/test_x_air_pur.py000066400000000000000000000124531464012051300226760ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync.py000066400000000000000000000136011464012051300225400ustar00rootroot00000000000000"""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() webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_10a.py000066400000000000000000000154041464012051300232040ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_15a.py000066400000000000000000000166421464012051300232160ustar00rootroot00000000000000from 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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_7aswitch.py000066400000000000000000000146331464012051300243570ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_bulbs.py000066400000000000000000000315231464012051300237320ustar00rootroot00000000000000from 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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_devices.py000066400000000000000000000271631464012051300242520ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_login.py000066400000000000000000000056611464012051300237370ustar00rootroot00000000000000"""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' webdjoe-pyvesync-d437c42/src/tests/test_x_vesync_outdoor.py000066400000000000000000000157341464012051300243240ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/src/tests/test_x_wall_switch.py000066400000000000000000000071161464012051300235550ustar00rootroot00000000000000import 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() webdjoe-pyvesync-d437c42/src/tests/utils.py000066400000000000000000000311201464012051300207770ustar00rootroot00000000000000"""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 webdjoe-pyvesync-d437c42/tox.ini000066400000000000000000000007571464012051300166630ustar00rootroot00000000000000[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 commands = mypy src/pyvesync