Skip to content

Commit 3ca1501

Browse files
authored
Merge pull request #238 from webdjoe/everest-rebase
Add Support for Everest Device
2 parents ccc2327 + 74c07da commit 3ca1501

File tree

8 files changed

+636
-255
lines changed

8 files changed

+636
-255
lines changed

README.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ pyvesync is a library to manage VeSync compatible [smart home devices](#supporte
2929
- [Standard Air Purifier Properties \& Methods](#standard-air-purifier-properties--methods)
3030
- [Air Purifier Properties](#air-purifier-properties)
3131
- [Air Purifier Methods](#air-purifier-methods)
32-
- [Levoit Purifier Core200S/300S/400S and Vital 100S/200S Properties](#levoit-purifier-core200s300s400s-and-vital-100s200s-properties)
33-
- [Levoit Purifier Core200S/300S/400S and Vital 100S/200S Methods](#levoit-purifier-core200s300s400s-and-vital-100s200s-methods)
34-
- [Levoit Vital 100S/200S Properties and Methods](#levoit-vital-100s200s-properties-and-methods)
32+
- [Levoit Purifier Core200S/300S/400S and Vital 100S/200S Properties](#levoit-purifier-core200s300s400s-vital-100s200s--everest-air-properties)
33+
- [Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Methods](#levoit-purifier-core200s300s400s-vital-100s200s--everest-air-methods)
34+
- [Levoit Vital 100S/200S Properties and Methods](#levoit-vital-100s200s--everest-air-properties-and-methods)
3535
- [Lights API Methods \& Properties](#lights-api-methods--properties)
3636
- [Brightness Light Bulb Method and Properties](#brightness-light-bulb-method-and-properties)
3737
- [Light Bulb Color Temperature Methods and Properties](#light-bulb-color-temperature-methods-and-properties)
@@ -94,6 +94,7 @@ pip install pyvesync
9494
5. Core 600S
9595
6. Vital 100S
9696
7. Vital 200S
97+
8. Everest Air
9798

9899
### Etekcity Bulbs
99100

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

333-
#### Levoit Purifier Core200S/300S/400S and Vital 100S/200S Properties
334+
#### Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Properties
334335

335336
`VeSyncFan.child_lock` - Return the state of the child lock (True=On/False=off)
336337

337338
`VeSyncAir.night_light` - Return the state of the night light (on/dim/off) **Not available on Vital 100S/200S**
338339

339-
#### Levoit Purifier Core200S/300S/400S and Vital 100S/200S Methods
340+
#### Levoit Purifier Core200S/300S/400S, Vital 100S/200S & Everest Air Methods
340341

341342
`VeSyncFan.child_lock_on()` Enable child lock
342343

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

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

351-
`VeSyncFan.get_timer()` - Get any running timers, stores Timer DataClass in `VeSyncFan.timer`
352+
`VeSyncFan.get_timer()` - Get any running timers, stores Timer DataClass in `VeSyncFan.timer`. See [Timer Dataclass](#timer-dataclass)
352353

353354
`VeSyncFan.set_timer(timer_duration=3000)` - Set a timer for the device, only turns device off. Timer DataClass stored in `VeSyncFan.timer`
354355

355356
`VeSyncFan.clear_timer()` - Cancel any running timer
356357

357358
`VeSyncFan.reset_filter()` - Reset filter to 100% **NOTE: Only available on Core200S**
358359

359-
#### Levoit Vital 100S/200S Properties and Methods
360+
#### Levoit Vital 100S/200S & Everest Air Properties and Methods
360361

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

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

377378
`VeSyncFan.set_light_detection_off()` - Turn off light detection mode
378379

380+
#### Levoit Everest Air Properties & Methods
381+
382+
`VeSyncFan.turbo_mode()` - Set turbo mode
383+
384+
Additional properties in the `VeSyncFan['details']` dictionary:
385+
386+
```python
387+
VeSyncFan['Details'] = {
388+
'pm1': 0, # air quality reading of particulates 1.0 microns
389+
'pm10': 10, # air quality reading of particulates 10 microns
390+
'fan_rotate_angle': 45, # angle of fan vents
391+
'aq_percent': 45, # Air Quality percentage reading
392+
'filter_open_state': False # returns bool of filter open
393+
}
394+
395+
```
396+
379397
### Lights API Methods & Properties
380398

381399
#### Brightness Light Bulb Method and Properties
@@ -1029,7 +1047,7 @@ After:
10291047
}
10301048
```
10311049

1032-
# Contributing
1050+
## Contributing
10331051

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

azure-pipelines.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ jobs:
1717
steps:
1818
- task: UsePythonVersion@0
1919
inputs:
20-
versionSpec: '3.8'
21-
displayName: 'Use Python 3.8'
20+
versionSpec: '3.9'
21+
displayName: 'Use Python 3.9'
2222
- script: |
2323
python -m pip install --upgrade pip
2424
pip install -r requirements.txt
@@ -42,12 +42,12 @@ jobs:
4242
vmImage: 'ubuntu-20.04'
4343
strategy:
4444
matrix:
45-
Python38:
46-
python.version: '3.8'
4745
Python39:
4846
python.version: '3.9'
4947
Python310:
5048
python.version: '3.10'
49+
Python311:
50+
python.version: '3.11'
5151

5252

5353
steps:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='pyvesync',
13-
version='2.1.10',
13+
version='2.1.11',
1414
description='pyvesync is a library to manage Etekcity\
1515
Devices, Cosori Air Fryers and Levoit Air \
1616
Purifiers run on the VeSync app.',

src/pyvesync/helpers.py

Lines changed: 36 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import colorsys
88
from dataclasses import dataclass, field, InitVar
9-
from typing import NamedTuple, Optional, Union
9+
from typing import Any, Dict, NamedTuple, Optional, Union
1010
import re
1111
import requests
1212

@@ -38,7 +38,7 @@ class Helpers:
3838
"""VeSync Helper Functions."""
3939

4040
@staticmethod
41-
def req_headers(manager) -> dict:
41+
def req_headers(manager) -> Dict[str, str]:
4242
"""Build header for api requests."""
4343
headers = {
4444
'accept-language': 'en',
@@ -51,25 +51,25 @@ def req_headers(manager) -> dict:
5151
return headers
5252

5353
@staticmethod
54-
def req_header_bypass() -> dict:
54+
def req_header_bypass() -> Dict[str, str]:
5555
"""Build header for api requests on 'bypass' endpoint."""
5656
return {
5757
'Content-Type': 'application/json; charset=UTF-8',
5858
'User-Agent': 'okhttp/3.12.1',
5959
}
6060

6161
@staticmethod
62-
def req_body_base(manager) -> dict:
62+
def req_body_base(manager) -> Dict[str, str]:
6363
"""Return universal keys for body of api requests."""
6464
return {'timeZone': manager.time_zone, 'acceptLanguage': 'en'}
6565

6666
@staticmethod
67-
def req_body_auth(manager) -> dict:
67+
def req_body_auth(manager) -> Dict[str, str]:
6868
"""Keys for authenticating api requests."""
6969
return {'accountID': manager.account_id, 'token': manager.token}
7070

7171
@staticmethod
72-
def req_body_details() -> dict:
72+
def req_body_details() -> Dict[str, str]:
7373
"""Detail keys for api requests."""
7474
return {
7575
'appVersion': APP_VERSION,
@@ -79,83 +79,57 @@ def req_body_details() -> dict:
7979
}
8080

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

8686
if type_ == 'login':
87-
body = {**cls.req_body_base(manager),
88-
**cls.req_body_details()}
89-
body['email'] = manager.username
90-
body['password'] = cls.hash_password(manager.password)
91-
body['devToken'] = ''
92-
body['userType'] = USER_TYPE
93-
body['method'] = 'login'
94-
elif type_ == 'devicedetail':
95-
body = {
96-
**cls.req_body_base(manager),
97-
**cls.req_body_auth(manager),
98-
**cls.req_body_details(),
99-
}
100-
body['method'] = 'devicedetail'
101-
body['mobileId'] = MOBILE_ID
102-
elif type_ == 'devicelist':
103-
body = {
104-
**cls.req_body_base(manager),
105-
**cls.req_body_auth(manager),
106-
**cls.req_body_details(),
107-
}
87+
body |= cls.req_body_details() # type: ignore
88+
body |= {
89+
'email': manager.username,
90+
'password': cls.hash_password(manager.password),
91+
'devToken': '',
92+
'userType': USER_TYPE,
93+
'method': 'login'
94+
} # type: ignore
95+
return body
96+
97+
body |= cls.req_body_auth(manager) # type: ignore
98+
99+
if type_ == 'devicestatus':
100+
return body
101+
102+
body |= cls.req_body_details() # type: ignore
103+
104+
if type_ == 'devicelist':
108105
body['method'] = 'devices'
109106
body['pageNo'] = '1'
110107
body['pageSize'] = '100'
111-
elif type_ == 'devicestatus':
112-
body = {**cls.req_body_base(manager),
113-
**cls.req_body_auth(manager)}
108+
109+
elif type_ == 'devicedetail':
110+
body['method'] = 'devicedetail'
111+
body['mobileId'] = MOBILE_ID
112+
114113
elif type_ == 'energy_week':
115-
body = {
116-
**cls.req_body_base(manager),
117-
**cls.req_body_auth(manager),
118-
**cls.req_body_details(),
119-
}
120114
body['method'] = 'energyweek'
121115
body['mobileId'] = MOBILE_ID
116+
122117
elif type_ == 'energy_month':
123-
body = {
124-
**cls.req_body_base(manager),
125-
**cls.req_body_auth(manager),
126-
**cls.req_body_details(),
127-
}
128118
body['method'] = 'energymonth'
129119
body['mobileId'] = MOBILE_ID
120+
130121
elif type_ == 'energy_year':
131-
body = {
132-
**cls.req_body_base(manager),
133-
**cls.req_body_auth(manager),
134-
**cls.req_body_details(),
135-
}
136122
body['method'] = 'energyyear'
137123
body['mobileId'] = MOBILE_ID
124+
138125
elif type_ == 'bypass':
139-
body = {
140-
**cls.req_body_base(manager),
141-
**cls.req_body_auth(manager),
142-
**cls.req_body_details(),
143-
}
144126
body['method'] = 'bypass'
127+
145128
elif type_ == 'bypassV2':
146-
body = {
147-
**cls.req_body_base(manager),
148-
**cls.req_body_auth(manager),
149-
**cls.req_body_details(),
150-
}
151129
body['deviceRegion'] = DEFAULT_REGION
152130
body['method'] = 'bypassV2'
131+
153132
elif type_ == 'bypass_config':
154-
body = {
155-
**cls.req_body_base(manager),
156-
**cls.req_body_auth(manager),
157-
**cls.req_body_details(),
158-
}
159133
body['method'] = 'firmwareUpdateInfo'
160134

161135
return body

src/pyvesync/vesync.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,45 @@ def kitchen(dev_type, config, manager):
7474

7575

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

7979
def __init__(self, username, password, time_zone=DEFAULT_TZ,
8080
debug=False, redact=True):
81-
"""Initialize VeSync class with username, password and time zone."""
81+
"""Initialize VeSync Manager.
82+
83+
This class is used as the manager for all VeSync objects, all methods and
84+
API calls are performed from this class. Time zone, debug and redact are
85+
optional. Time zone must be a string of an IANA time zone format. Once
86+
class is instantiated, call `manager.login()` to log in to VeSync servers,
87+
which returns `True` if successful. Once logged in, call `manager.update()`
88+
to retrieve devices and update device details.
89+
90+
Parameters:
91+
-----------
92+
username : str
93+
VeSync account username (usually email address)
94+
password : str
95+
VeSync account password
96+
time_zone : str, optional
97+
Time zone for device from IANA database, by default DEFAULT_TZ
98+
debug : bool, optional
99+
Enable debug logging, by default False
100+
redact : bool, optional
101+
Redact sensitive information in logs, by default True
102+
103+
Attributes
104+
----------
105+
fans : list
106+
List of VeSyncFan objects for humidifiers and air purifiers
107+
outlets : list
108+
List of VeSyncOutlet objects for smart plugs
109+
switches : list
110+
List of VeSyncSwitch objects for wall switches
111+
bulbs : list
112+
List of VeSyncBulb objects for smart bulbs
113+
kitchen : list
114+
List of VeSyncKitchen objects for smart kitchen appliances
115+
"""
82116
self.debug = debug
83117
if debug: # pragma: no cover
84118
logger.setLevel(logging.DEBUG)
@@ -308,7 +342,15 @@ def get_devices(self) -> bool:
308342
return proc_return
309343

310344
def login(self) -> bool:
311-
"""Return True if log in request succeeds."""
345+
"""Log into VeSync server.
346+
347+
Username and password are provided when class is instantiated.
348+
349+
Returns
350+
-------
351+
bool
352+
True if login successful, False if not
353+
"""
312354
user_check = isinstance(self.username, str) and len(self.username) > 0
313355
pass_check = isinstance(self.password, str) and len(self.password) > 0
314356
if user_check is False:
@@ -346,7 +388,16 @@ def device_time_check(self) -> bool:
346388
return False
347389

348390
def update(self) -> None:
349-
"""Fetch updated information about devices."""
391+
"""Fetch updated information about devices.
392+
393+
Pulls devices list from VeSync and instantiates any new devices. Devices
394+
are stored in the instance attributes `outlets`, `switches`, `fans`, and
395+
`bulbs`. The `_device_list` attribute is a dictionary of these attributes.
396+
397+
Returns
398+
-------
399+
None
400+
"""
350401
if self.device_time_check():
351402

352403
if not self.enabled:

0 commit comments

Comments
 (0)