pyvesync is a library to manage VeSync compatible smart home devices
Check out the new pyvesync documentation for usage and full API details.
- Outlets
- Switches
- Fans
- Air Purifiers
- Humidifiers
- Bulbs
- Air Fryers
- Thermostats
See the supported devices page for a complete list of supported devices and device types.
BREAKING CHANGES - The release of pyvesync 3.0 comes with many improvements and new features, but as a result there are many breaking changes. The structure has been completely refactored, so please read through the README and thoroughly test before deploying.
The goal is to standardize the library across all devices to allow easier and consistent maintainability moving forward. The original library was created 8 years ago for supporting only a few outlets, it was not designed for supporting 20+ different devices.
Some of the changes are:
- Asynchronous network requests with aiohttp.
- Strong typing of all network requests and responses.
- Created base classes for all devices for easier
isinstance
checks. - Separated the instantiated devices to a
DeviceContainer
class that acts as a mutable set with convenience methods. - Standardized the API for all device to follow a common naming convention. No more devices with different names for the same functionality.
- Implemented custom exceptions and error (code) handling for API responses.
const
module to hold all library constants- Built the
DeviceMap
class to hold the mapping and features of devices. - COMING SOON: Use API to pull device modes and operating features.
See pyvesync V3 for more information on the changes.
Library is now asynchronous, using aiohttp as a replacement for requests. The pyvesync.VeSync
class is an asynchronous context manager. A aiohttp.ClientSession
can be passed or created internally.
import asyncio
import aiohttp
from pyvesync.vesync import VeSync
async def main():
async with VeSync(
username="user",
password="password",
country_code="US", # Optional - country Code to select correct server
session=session, # Optional - aiohttp.ClientSession
time_zone="America/New_York", # Optional - Timezone, defaults to America/New_York
debug=False, # Optional - Debug output
redact=True # Optional - Redact sensitive information from logs
) as manager:
# To enable debug mode - prints request and response content for
# api calls that return an error code
manager.debug = True
# Redact mode is enabled by default, set to False to disable
manager.redact = False
# To print request & response content for all API calls enable verbose mode
manager.verbose = True
# To print logs to file
manager.log_to_file("pyvesync.log")
await manager.login()
if not manager.enabled:
print("Not logged in.")
return
await manager.get_devices() # Instantiates supported devices in device list, automatically called by login, only needed if you would like updates
await manager.update() # Updates the state of all devices
# manager.devices is a DeviceContainer object
# manager.devices.outlets is a list of VeSyncOutlet objects
# manager.devices.switches is a list of VeSyncSwitch objects
# manager.devices.fans is a list of VeSyncFan objects
# manager.devices.bulbs is a list of VeSyncBulb objects
# manager.devices.humidifiers is a list of VeSyncHumid objects
# manager.devices.air_purifiers is a list of VeSyncAir objects
# manager.devices.air_fryers is a list of VeSyncAirFryer objects
# manager.devices.thermostats is a list of VeSyncThermostat objects
for outlet in manager.devices.outlets:
# The outlet object contain all action methods and static device attributes
await outlet.update()
await outlet.turn_off()
outlet.display() # Print static device information, name, type, CID, etc.
# State of object held in `device.state` attribute
print(outlet.state)
state_json = outlet.dumps() # Returns JSON string of device state
state_bytes = orjson.dumps(outlet.state) # Returns bytes of device state
# to view the response information of the last API call
print(outlet.last_response)
# Prints a ResponseInfo object containing error code,
# and other response information
# Or use your own session
session = aiohttp.ClientSession()
async def main():
async with VeSync("user", "password", session=session):
await manager.login()
await manager.update()
if __name__ == "__main__":
asyncio.run(main())
If using async with
is not ideal, the __aenter__()
and __aexit__()
methods need to be called manually:
manager = VeSync(user, password)
await manager.__aenter__()
...
await manager.__aexit__(None, None, None)
pvesync will close the ClientSession
that was created by the library on __aexit__
. If a session is passed in as an argument, the library does not close it. If a session is passed in and not closed, aiohttp will generate an error on exit:
2025-02-16 14:41:07 - ERROR - asyncio - Unclosed client session
2025-02-16 14:41:07 - ERROR - asyncio - Unclosed connector
The VeSync signature is:
VeSync(
username: str,
password: str,
session: ClientSession | None = None,
time_zone: str = DEFAULT_TZ # America/New_York
)
The VeSync class no longer accepts a debug
or redact
argument. To set debug the library set manager.debug = True
to the instance and manager.redact = True
.
There is a new nomenclature for product types that defines the device class. The
device.product_type
attribute defines the product type based on the VeSync API. The product type is used to determine the device class and module. The currently supported product types are:
outlet
- Outlet devicesswitch
- Wall switchesfan
- Fans (not air purifiers or humidifiers)purifier
- Air purifiers (not humidifiers)humidifier
- Humidifiers (not air purifiers)bulb
- Light bulbs (not dimmers or switches)airfryer
- Air fryers
See Supported Devices for a complete list of supported devices and models.
Exceptions are no longer caught by the library and must be handled by the user. Exceptions are raised by server errors and aiohttp connection errors.
Errors that occur at the aiohttp level are raised automatically and propogated to the user. That means exceptions raised by aiohttp that inherit from aiohttp.ClientError
are propogated.
When the connection to the VeSync API succeeds but returns an error code that prevents the library from functioning a custom exception inherrited from pyvesync.logs.VeSyncError
is raised.
Custom Exceptions raised by all API calls:
pyvesync.logs.VeSyncServerError
- The API connected and returned a code indicated there is a server-side error.pyvesync.logs.VeSyncRateLimitError
- The API's rate limit has been exceeded.pyvesync.logs.VeSyncAPIStatusCodeError
- The API returned a non-200 status code.pyvesync.logs.VeSyncAPIResponseError
- The response from the API was not in an expected format.
Login API Exceptions
pyvesync.logs.VeSyncLoginError
- The username or password is incorrect.
See errors documentation for a complete list of error codes and exceptions.
The raise_api_errors() function is called for every API call and checks for general response errors. It can raise the following exceptions:
VeSyncServerError
- The API connected and returned a code indicated there is a server-side error.VeSyncRateLimitError
- The API's rate limit has been exceeded.VeSyncAPIStatusCodeError
- The API returned a non-200 status code.VeSyncTokenError
- The API returned a token error and requireslogin()
to be called again.VeSyncLoginError
- The username or password is incorrect.
Install the latest version from pip:
pip install pyvesync
- Voltson Smart WiFi Outlet- Round (7A model ESW01-USA)
- Voltson Smart WiFi Outlet - Round (10A model ESW01-EU)
- Voltson Smart Wifi Outlet - Round (10A model ESW03-USA)
- Voltson Smart Wifi Outlet - Round (10A model ESW10-USA)
- Voltson Smart WiFi Outlet - Rectangle (15A model ESW15-USA)
- Two Plug Outdoor Outlet (ESO15-TB) (Each plug is a separate
VeSyncOutlet
object, energy readings are for both plugs combined)
- Etekcity Smart WiFi Light Switch (model ESWL01)
- Etekcity Wifi Dimmer Switch (ESD16)
- LV-PUR131S
- Core 200S
- Core 300S
- Core 400S
- Core 600S
- Vital 100S
- Vital 200S
- Everest Air
- Soft White Dimmable Smart Bulb (ESL100)
- Cool to Soft White Tunable Dimmable Bulb (ESL100CW)
- Valceno Multicolor Bulb (XYD0001)
- Dual 200S
- Classic 300S
- LV600S
- OasisMist 450S
- OasisMist 600S
- OasisMist 1000S
- Cosori 3.7 and 5.8 Quart Air Fryer
- 42 in. Tower Fan
import asyncio
from pyvesync import VeSync
from pyvesync.logs import VeSyncLoginError
# VeSync is an asynchronous context manager
# VeSync(username, password, debug=False, redact=True, session=None)
async def main():
async with VeSync("user", "password") as manager:
await manager.login()
await manager.update()
# Acts as a set of device instances
device_container = manager.devices
outlets = device_container.outlets # List of outlet instances
outlet = outlets[0]
await outlet.update()
await outlet.turn_off()
outlet.display()
# Iterate of entire device list
for devices in device_container:
device.display()
if __name__ == "__main__":
asyncio.run(main())
Devices are stored in the respective lists in the instantiated VeSync
class:
await manager.login() # Asynchronous
await manager.update() # Asynchronous
# Acts as set with properties that return product type lists
manager.devices = DeviceContainer instance
manager.devices.outlets = [VeSyncOutletInstances]
manager.devices.switches = [VeSyncSwitchInstances]
manager.devices.fans = [VeSyncFanInstances]
manager.devices.bulbs = [VeSyncBulbInstances]
manager.devices.air_purifiers = [VeSyncPurifierInstances]
manager.devices.humidifiers = [VeSyncHumidifierInstances]
manager.devices.air_fryers = [VeSyncAirFryerInstances]
managers.devices.thermostats = [VeSyncThermostatInstances]
# Get device by device name
dev_name = "My Device"
for device in manager.devices:
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.devices.switches:
if switch.device_name == switch_name:
await switch.turn_on() # Asynchronous
See the device documentation for more information on the device classes and their methods/states.
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.
The redact
argument removes any tokens and account identifiers from the output to allow for easier sharing. The redact
argument has no impact if debug
is not True
.
import asyncio
import aiohttp
from pyvesync.vesync import VeSync
async def main():
async with VeSync("user", "password") as manager:
manager.debug = True
manager.redact = True # True by default
await manager.login()
await manager.update()
outlet = manager.outlets[0]
await outlet.update()
await outlet.turn_off()
outlet.display()
if __name__ == "__main__":
asyncio.run(main())
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.
mkdir python_test && cd python_test
# Check Python version is 3.11 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.git@BRANCHNAME
# Install a PR that has not been merged:
pip install git+https://github.com/webdjoe/pyvesync.git@refs/pull/PR_NUMBER/head
Test functionality with a script, please adjust methods and logging statements to the device you are testing.
test.py
import asyncio
import sys
import logging
import json
from functool import chain
from pyvesync import VeSync
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
USERNAME = "YOUR USERNAME"
PASSWORD = "YOUR PASSWORD"
DEVICE_NAME = "Device" # Device to test
async def test_device():
# Instantiate VeSync class and login
async with VeSync(USERNAME, PASSWORD, debug=True, redact=True) as manager:
await manager.login()
# Pull and update devices
await manager.update()
for dev in manager.devices:
# 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():
logger.debug("%s is not %s, continuing", self.device_name, DEVICE_NAME)
continue
logger.debug('--------------%s-----------------' % dev.device_name)
logger.debug(dev.display())
logger.debug(dev.displayJSON())
# Test all device methods and functionality
# Test Properties
logger.debug("Fan is on - %s", dev.is_on)
logger.debug("Modes - %s", dev.modes)
logger.debug("Fan Level - %s", dev.fan_level)
logger.debug("Fan Air Quality - %s", dev.air_quality)
logger.debug("Screen Status - %s", dev.screen_status)
logger.debug("Turning on")
await fan.turn_on()
logger.debug("Device is on %s", dev.is_on)
logger.debug("Turning off")
await fan.turn_off()
logger.debug("Device is on %s", dev.is_on)
logger.debug("Sleep mode")
fan.sleep_mode()
logger.debug("Current mode - %s", dev.details['mode'])
fan.auto_mode()
logger.debug("Set Fan Speed - %s", dev.set_fan_speed)
logger.debug("Current Fan Level - %s", dev.fan_level)
logger.debug("Current mode - %s", dev.mode)
# Display all device info
logger.debug(dev.display(state=True))
logger.debug(dev.to_json(state=True, indent=True))
dev_dict = dev.to_dict(state=True)
if __name__ == "__main__":
logger.debug("Testing device")
asyncio.run(test_device())
...
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 or Frida. 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:
{
"tk": "abc123abc123==3rf",
"accountId": "123456789",
"cid": "abcdef12-3gh-ij"
}
After:
{
"tk": "AAA111AAA111==1AA",
"accountId": "111111111",
"cid": "AAAAAA11-1AA-AA"
}
All contributions are welcome.
This project is licensed under MIT.
This is an open source project and cannot exist without the contributions of its community. Thank you to all the contributors who have helped make this project better!
A special thanks for helping with V3 go live:
![]() cdninja |
![]() sapuseven |
![]() sdrapha |
---|
And to all of those that contributed to the project:
Made with contrib.rocks.