Skip to content

Commit a7f7243

Browse files
authored
Merge pull request #363 from webdjoe/dev
feat: Authentication process improvement Fix Core Details Model bug Fix NightlightMode enum Cleanup CI/CD process
2 parents f54e1a0 + afe0e91 commit a7f7243

22 files changed

+897
-269
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ overrides/
3838
testing_scripts/api_test_editor.py
3939
testing_scripts/yamltest.yaml
4040
testing_scripts/yamltest copy.yaml
41+
creds.json

.pre-commit-config.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ repos:
66
- id: check-yaml
77
- id: trailing-whitespace
88
- id: end-of-file-fixer
9+
- id: check-ast
10+
- id: check-toml
911
- repo: https://github.com/pre-commit/mirrors-mypy
1012
rev: v1.17.1
1113
hooks:
@@ -14,6 +16,6 @@ repos:
1416
rev: v0.12.12
1517
hooks:
1618
- id: ruff-check
17-
args: [ --fix, --config=./.ruff.toml ]
19+
args: [ --fix, --config=ruff.toml ]
1820
- id: ruff-format
19-
args: [ --config=./.ruff.toml ]
21+
args: [ --config=ruff.toml ]

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,14 @@ The VeSync signature is:
146146
VeSync(
147147
username: str,
148148
password: str,
149+
country_code: str = DEFAULT_COUNTRY_CODE, # US
149150
session: ClientSession | None = None,
150151
time_zone: str = DEFAULT_TZ # America/New_York
152+
debug: bool = False,
153+
redact: bool = True,
151154
)
152155
```
153156

154-
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`.
155-
156157
### Product Types
157158

158159
There is a new nomenclature for product types that defines the device class. The
@@ -299,6 +300,59 @@ async def main():
299300
device.display()
300301

301302

303+
if __name__ == "__main__":
304+
asyncio.run(main())
305+
```
306+
307+
If you want to reuse your token and account_id between runs. The `VeSync.auth` object holds the credentials and helper methods to save and load credentials.
308+
309+
```python
310+
import asyncio
311+
from pyvesync import VeSync
312+
from pyvesync.logs import VeSyncLoginError
313+
314+
# VeSync is an asynchronous context manager
315+
# VeSync(username, password, debug=False, redact=True, session=None)
316+
317+
async def main():
318+
async with VeSync("user", "password") as manager:
319+
320+
# If credentials are stored in a file, it can be loaded
321+
# the default location is ~/.vesync_token
322+
await manager.load_credentials_from_file()
323+
# or the file path can be passed
324+
await manager.load_credentials_from_file("/path/to/token_file")
325+
326+
# Or credentials can be passed directly
327+
manager.set_credentials("your_token", "your_account_id")
328+
329+
# No login needed
330+
# await manager.login()
331+
332+
# To store credentials to a file after login
333+
await manager.save_credentials() # Saves to default location ~/.vesync_token
334+
# or pass a file path
335+
await manager.save_credentials("/path/to/token_file")
336+
337+
# Output Credentials as JSON String
338+
await manager.output_credentials()
339+
340+
await manager.update()
341+
342+
# Acts as a set of device instances
343+
device_container = manager.devices
344+
345+
outlets = device_container.outlets # List of outlet instances
346+
outlet = outlets[0]
347+
await outlet.update()
348+
await outlet.turn_off()
349+
outlet.display()
350+
351+
# Iterate of entire device list
352+
for devices in device_container:
353+
device.display()
354+
355+
302356
if __name__ == "__main__":
303357
asyncio.run(main())
304358
```

azure-pipelines.yml

Lines changed: 0 additions & 67 deletions
This file was deleted.

docs/authentication.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# VeSync Authentication Module
2+
3+
The VeSync Authentication Module provides a clean separation of authentication logic from the main VeSync class, offering improved maintainability, better error handling, and additional features like token persistence.
4+
5+
## Features
6+
7+
- **Flexible Authentication**: Support for both username/password and token-based authentication
8+
- **Token Persistence**: Automatic saving and loading of authentication tokens to/from disk
9+
- **Improved Error Handling**: More granular and descriptive error messages
10+
- **Better Security**: Secure file permissions for token storage
11+
- **Graceful Token Validation**: Automatic validation of existing tokens before use
12+
- **Cross-Region Support**: Automatic handling of region changes during authentication
13+
14+
## Usage
15+
16+
### Basic Username/Password Authentication
17+
18+
```python
19+
import asyncio
20+
from pyvesync import VeSync
21+
22+
async def main():
23+
manager = VeSync(
24+
username="[email protected]",
25+
password="your_password",
26+
country_code="US"
27+
)
28+
29+
# Login
30+
success = await manager.login()
31+
if success:
32+
print("Login successful!")
33+
# Use manager for device operations
34+
await manager.get_devices()
35+
await manager.update_all_devices()
36+
37+
await manager.__aexit__()
38+
39+
asyncio.run(main())
40+
```
41+
42+
### Token-Based Authentication
43+
44+
```python
45+
import asyncio
46+
from pyvesync import VeSync
47+
48+
async def main():
49+
# Use existing token (e.g., from previous login)
50+
manager = VeSync(
51+
token="your_existing_token",
52+
account_id="your_account_id",
53+
country_code="US"
54+
)
55+
56+
# Login with token
57+
success = await manager.login()
58+
if success:
59+
print("Token authentication successful!")
60+
61+
await manager.__aexit__()
62+
63+
asyncio.run(main())
64+
```
65+
66+
### Persistent Token Storage
67+
68+
```python
69+
import asyncio
70+
from pathlib import Path
71+
from pyvesync import VeSync
72+
73+
async def main():
74+
token_file = Path.home() / ".vesync_token"
75+
76+
manager = VeSync(
77+
username="[email protected]",
78+
password="your_password",
79+
token_file_path=token_file
80+
)
81+
82+
# First login saves token to file
83+
success = await manager.login()
84+
if success:
85+
print("Login successful! Token saved for future use.")
86+
87+
# Subsequent runs will automatically load the saved token
88+
# and validate it before falling back to username/password
89+
90+
await manager.__aexit__()
91+
92+
asyncio.run(main())
93+
```
94+
95+
### Direct Authentication Management
96+
97+
```python
98+
from pyvesync import VeSync
99+
100+
# Create manager without initial credentials
101+
manager = VeSync()
102+
103+
# Set credentials programmatically
104+
manager.auth.set_credentials(
105+
username="[email protected]",
106+
password="your_password"
107+
)
108+
109+
# Check authentication state
110+
print(f"Is authenticated: {manager.auth.is_authenticated}")
111+
print(f"Username: {manager.auth.username}")
112+
113+
# Clear credentials
114+
manager.auth.clear_credentials()
115+
```
116+
117+
## Authentication Flow
118+
119+
The authentication process follows these steps:
120+
121+
1. **Token Validation**: If a token exists, validate it first
122+
2. **Username/Password Login**: If no valid token, use credentials
123+
3. **Authorization Code Exchange**: Get auth code, then exchange for token
124+
4. **Cross-Region Handling**: Automatically handle region changes
125+
5. **Token Persistence**: Save token to file if path is provided
126+
127+
## VeSyncAuth Class
128+
129+
The `VeSyncAuth` class handles all authentication logic:
130+
131+
### Properties
132+
133+
- `token`: Authentication token
134+
- `account_id`: VeSync account ID
135+
- `country_code`: Country code for the account
136+
- `username`: Account username (read-only)
137+
- `password`: Account password (read-only)
138+
- `is_authenticated`: Boolean indicating if user is authenticated
139+
140+
### Methods
141+
142+
- `login()`: Perform authentication
143+
- `set_credentials()`: Set authentication credentials
144+
- `clear_credentials()`: Clear all stored credentials
145+
- `get_auth_headers()`: Get headers for authenticated API requests
146+
- `to_dict()`: Get authentication state as dictionary
147+
148+
## Migration from Old VeSync Class
149+
150+
The new authentication module is fully backward compatible. Existing code will continue to work:
151+
152+
```python
153+
# Old way (still works)
154+
manager = VeSync("[email protected]", "password")
155+
await manager.login()
156+
157+
# New way (recommended)
158+
manager = VeSync(
159+
username="[email protected]",
160+
password="password",
161+
token_file_path="~/.vesync_token"
162+
)
163+
await manager.login()
164+
```
165+
166+
## Advanced Features
167+
168+
### Custom Token File Location
169+
170+
```python
171+
from pathlib import Path
172+
173+
# Custom location
174+
token_file = Path("/secure/location/vesync_token.json")
175+
manager = VeSync(
176+
username="[email protected]",
177+
password="password",
178+
token_file_path=token_file
179+
)
180+
```
181+
182+
### Token-Only Authentication
183+
184+
```python
185+
# For applications that manage tokens externally
186+
manager = VeSync(
187+
token="externally_managed_token",
188+
account_id="known_account_id"
189+
)
190+
```
191+
192+
### Error Handling
193+
194+
```python
195+
from pyvesync.utils.errors import VeSyncLoginError, VeSyncAPIResponseError
196+
197+
try:
198+
success = await manager.login()
199+
except VeSyncLoginError as e:
200+
print(f"Login failed: {e}")
201+
except VeSyncAPIResponseError as e:
202+
print(f"API error: {e}")
203+
```
204+
205+
## Security Considerations
206+
207+
- Token files are created with restrictive permissions (0o600)
208+
- Sensitive information is not included in string representations
209+
- Credentials are cleared from memory when `clear_credentials()` is called
210+
- Token validation prevents use of expired tokens
211+
212+
## Thread Safety
213+
214+
The authentication module is designed for use with asyncio and is not thread-safe. Use appropriate synchronization if accessing from multiple threads.

0 commit comments

Comments
 (0)