Skip to content

Commit

Permalink
Merge pull request #238 from webdjoe/everest-rebase
Browse files Browse the repository at this point in the history
Add Support for Everest Device
  • Loading branch information
webdjoe authored Jun 29, 2024
2 parents ccc2327 + 74c07da commit 3ca1501
Show file tree
Hide file tree
Showing 8 changed files with 636 additions and 255 deletions.
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ pyvesync is a library to manage VeSync compatible [smart home devices](#supporte
- [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-and-vital-100s200s-properties)
- [Levoit Purifier Core200S/300S/400S and Vital 100S/200S Methods](#levoit-purifier-core200s300s400s-and-vital-100s200s-methods)
- [Levoit Vital 100S/200S Properties and Methods](#levoit-vital-100s200s-properties-and-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)
Expand Down Expand Up @@ -94,6 +94,7 @@ pip install pyvesync
5. Core 600S
6. Vital 100S
7. Vital 200S
8. Everest Air

### Etekcity Bulbs

Expand Down Expand Up @@ -330,13 +331,13 @@ Compatible levels for each model:
- PUR131S [1, 2, 3]
- Vital 100S/200S [1, 2, 3, 4]

#### Levoit Purifier Core200S/300S/400S and Vital 100S/200S Properties
#### 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 and Vital 100S/200S Methods
#### Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Methods

`VeSyncFan.child_lock_on()` Enable child lock

Expand All @@ -348,15 +349,15 @@ Compatible levels for each model:

`VeSyncFan.set_night_light('on'|'dim'|'off')` - Set night light brightness

`VeSyncFan.get_timer()` - Get any running timers, stores Timer DataClass in `VeSyncFan.timer`
`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 Properties and Methods
#### Levoit Vital 100S/200S & Everest Air Properties and Methods

The Levoit Vital 100S/200S has additional features not available on other models.

Expand All @@ -376,6 +377,23 @@ The Levoit Vital 100S/200S has additional features not available on other models

`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
Expand Down Expand Up @@ -1029,7 +1047,7 @@ After:
}
```

# Contributing
## Contributing

All [contributions](CONTRIBUTING.md) are welcome.

Expand Down
8 changes: 4 additions & 4 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
displayName: 'Use Python 3.8'
versionSpec: '3.9'
displayName: 'Use Python 3.9'
- script: |
python -m pip install --upgrade pip
pip install -r requirements.txt
Expand All @@ -42,12 +42,12 @@ jobs:
vmImage: 'ubuntu-20.04'
strategy:
matrix:
Python38:
python.version: '3.8'
Python39:
python.version: '3.9'
Python310:
python.version: '3.10'
Python311:
python.version: '3.11'


steps:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name='pyvesync',
version='2.1.10',
version='2.1.11',
description='pyvesync is a library to manage Etekcity\
Devices, Cosori Air Fryers and Levoit Air \
Purifiers run on the VeSync app.',
Expand Down
98 changes: 36 additions & 62 deletions src/pyvesync/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import colorsys
from dataclasses import dataclass, field, InitVar
from typing import NamedTuple, Optional, Union
from typing import Any, Dict, NamedTuple, Optional, Union
import re
import requests

Expand Down Expand Up @@ -38,7 +38,7 @@ class Helpers:
"""VeSync Helper Functions."""

@staticmethod
def req_headers(manager) -> dict:
def req_headers(manager) -> Dict[str, str]:
"""Build header for api requests."""
headers = {
'accept-language': 'en',
Expand All @@ -51,25 +51,25 @@ def req_headers(manager) -> dict:
return headers

@staticmethod
def req_header_bypass() -> dict:
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:
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:
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:
def req_body_details() -> Dict[str, str]:
"""Detail keys for api requests."""
return {
'appVersion': APP_VERSION,
Expand All @@ -79,83 +79,57 @@ def req_body_details() -> dict:
}

@classmethod
def req_body(cls, manager, type_) -> dict:
def req_body(cls, manager, type_) -> Dict[str, Any]:
"""Builder for body of api requests."""
body = {}
body = cls.req_body_base(manager)

if type_ == 'login':
body = {**cls.req_body_base(manager),
**cls.req_body_details()}
body['email'] = manager.username
body['password'] = cls.hash_password(manager.password)
body['devToken'] = ''
body['userType'] = USER_TYPE
body['method'] = 'login'
elif type_ == 'devicedetail':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'devicedetail'
body['mobileId'] = MOBILE_ID
elif type_ == 'devicelist':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
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_ == 'devicestatus':
body = {**cls.req_body_base(manager),
**cls.req_body_auth(manager)}

elif type_ == 'devicedetail':
body['method'] = 'devicedetail'
body['mobileId'] = MOBILE_ID

elif type_ == 'energy_week':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'energyweek'
body['mobileId'] = MOBILE_ID

elif type_ == 'energy_month':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'energymonth'
body['mobileId'] = MOBILE_ID

elif type_ == 'energy_year':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'energyyear'
body['mobileId'] = MOBILE_ID

elif type_ == 'bypass':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'bypass'

elif type_ == 'bypassV2':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['deviceRegion'] = DEFAULT_REGION
body['method'] = 'bypassV2'

elif type_ == 'bypass_config':
body = {
**cls.req_body_base(manager),
**cls.req_body_auth(manager),
**cls.req_body_details(),
}
body['method'] = 'firmwareUpdateInfo'

return body
Expand Down
59 changes: 55 additions & 4 deletions src/pyvesync/vesync.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,45 @@ def kitchen(dev_type, config, manager):


class VeSync: # pylint: disable=function-redefined
"""VeSync API functions."""
"""VeSync Manager Class."""

def __init__(self, username, password, time_zone=DEFAULT_TZ,
debug=False, redact=True):
"""Initialize VeSync class with username, password and time zone."""
"""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)
Expand Down Expand Up @@ -308,7 +342,15 @@ def get_devices(self) -> bool:
return proc_return

def login(self) -> bool:
"""Return True if log in request succeeds."""
"""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:
Expand Down Expand Up @@ -346,7 +388,16 @@ def device_time_check(self) -> bool:
return False

def update(self) -> None:
"""Fetch updated information about devices."""
"""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:
Expand Down
Loading

0 comments on commit 3ca1501

Please sign in to comment.