pyvesync 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvesync/__init__.py +5 -0
- pyvesync/base_devices/__init__.py +35 -0
- pyvesync/base_devices/bulb_base.py +330 -0
- pyvesync/base_devices/fan_base.py +392 -0
- pyvesync/base_devices/fryer_base.py +68 -0
- pyvesync/base_devices/humidifier_base.py +462 -0
- pyvesync/base_devices/outlet_base.py +327 -0
- pyvesync/base_devices/purifier_base.py +525 -0
- pyvesync/base_devices/switch_base.py +271 -0
- pyvesync/base_devices/thermostat_base.py +283 -0
- pyvesync/base_devices/vesyncbasedevice.py +572 -0
- pyvesync/const.py +825 -0
- pyvesync/device_container.py +373 -0
- pyvesync/device_map.py +1172 -0
- pyvesync/devices/__init__.py +1 -0
- pyvesync/devices/vesyncbulb.py +1126 -0
- pyvesync/devices/vesyncfan.py +233 -0
- pyvesync/devices/vesynchumidifier.py +808 -0
- pyvesync/devices/vesynckitchen.py +651 -0
- pyvesync/devices/vesyncoutlet.py +954 -0
- pyvesync/devices/vesyncpurifier.py +990 -0
- pyvesync/devices/vesyncswitch.py +415 -0
- pyvesync/devices/vesyncthermostat.py +171 -0
- pyvesync/models/__init__.py +11 -0
- pyvesync/models/base_models.py +129 -0
- pyvesync/models/bulb_models.py +197 -0
- pyvesync/models/bypass_models.py +289 -0
- pyvesync/models/fan_models.py +47 -0
- pyvesync/models/fryer_models.py +34 -0
- pyvesync/models/home_models.py +169 -0
- pyvesync/models/humidifier_models.py +202 -0
- pyvesync/models/outlet_models.py +220 -0
- pyvesync/models/purifier_models.py +261 -0
- pyvesync/models/switch_models.py +101 -0
- pyvesync/models/thermostat_models.py +134 -0
- pyvesync/models/vesync_models.py +439 -0
- pyvesync/utils/__init__.py +1 -0
- pyvesync/utils/colors.py +250 -0
- pyvesync/utils/device_mixins.py +362 -0
- pyvesync/utils/enum_utils.py +26 -0
- pyvesync/utils/errors.py +867 -0
- pyvesync/utils/helpers.py +700 -0
- pyvesync/utils/logs.py +525 -0
- pyvesync/vesync.py +638 -0
- pyvesync/vesynchome.py +152 -0
- pyvesync-3.0.0.dist-info/METADATA +567 -0
- pyvesync-3.0.0.dist-info/RECORD +50 -0
- pyvesync-3.0.0.dist-info/WHEEL +5 -0
- pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
- pyvesync-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Base class for all VeSync devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import UTC
|
|
9
|
+
from datetime import datetime as dt
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
import orjson
|
|
13
|
+
from typing_extensions import deprecated
|
|
14
|
+
|
|
15
|
+
from pyvesync.const import ConnectionStatus, DeviceStatus
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pyvesync import VeSync
|
|
21
|
+
from pyvesync.device_map import DeviceMapTemplate
|
|
22
|
+
from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
|
|
23
|
+
from pyvesync.utils.errors import ResponseInfo
|
|
24
|
+
from pyvesync.utils.helpers import Timer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
VS_TYPE = TypeVar('VS_TYPE', bound='VeSyncBaseDevice')
|
|
28
|
+
VS_STATE_T = TypeVar('VS_STATE_T', bound='DeviceState')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VeSyncBaseDevice(ABC, Generic[VS_STATE_T]):
|
|
32
|
+
"""Properties shared across all VeSync devices.
|
|
33
|
+
|
|
34
|
+
Abstract Base Class for all VeSync devices. The device class is used solely
|
|
35
|
+
for operational methods and static device properties.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
details (ResponseDeviceDetailsModel): Device details from API call.
|
|
39
|
+
manager (VeSync): Manager object for API calls.
|
|
40
|
+
feature_map (DeviceMapTemplate): Device configuration map, will be specific
|
|
41
|
+
subclass of DeviceMapTemplate based on device type.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
state (pyvesync.base_devices.vesyncbasedevice.DeviceState): Device state object
|
|
45
|
+
Each device has a separate state base class in the base_devices module.
|
|
46
|
+
last_response (ResponseInfo): Last response from API call.
|
|
47
|
+
manager (VeSync): Manager object for API calls.
|
|
48
|
+
device_name (str): Name of device.
|
|
49
|
+
device_image (str): URL for device image.
|
|
50
|
+
cid (str): Device ID.
|
|
51
|
+
connection_type (str): Connection type of device.
|
|
52
|
+
device_type (str): Type of device.
|
|
53
|
+
type (str): Type of device.
|
|
54
|
+
uuid (str): UUID of device, not always present.
|
|
55
|
+
config_module (str): Configuration module of device.
|
|
56
|
+
mac_id (str): MAC ID of device.
|
|
57
|
+
current_firm_version (str): Current firmware version of device.
|
|
58
|
+
latest_firm_version (str | None): Latest firmware version of device.
|
|
59
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
60
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
61
|
+
sub_device_no (int): Sub-device number of device.
|
|
62
|
+
product_type (str): Product type of device.
|
|
63
|
+
features (dict): Features of device.
|
|
64
|
+
|
|
65
|
+
Methods:
|
|
66
|
+
set_timer: Set timer for device.
|
|
67
|
+
get_timer: Get timer for device from API.
|
|
68
|
+
clear_timer: Clear timer for device from API.
|
|
69
|
+
set_state: Set device state attribute.
|
|
70
|
+
get_state: Get device state attribute.
|
|
71
|
+
update: Update device details.
|
|
72
|
+
display: Print formatted static device info to stdout.
|
|
73
|
+
to_json: Print JSON API string
|
|
74
|
+
to_jsonb: JSON API bytes device details
|
|
75
|
+
to_dict: Return device information as a dictionary.
|
|
76
|
+
|
|
77
|
+
Note:
|
|
78
|
+
Device states are found in the `state` attribute in a subclass of DeviceState
|
|
79
|
+
based on the device type. The `DeviceState` subclass is located in device the
|
|
80
|
+
base_devices module.
|
|
81
|
+
|
|
82
|
+
The `last_response` attribute is used to store the last response and error
|
|
83
|
+
information from the API call. See the `pyvesync.errors` module for more
|
|
84
|
+
information.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
__slots__ = (
|
|
88
|
+
'__base_exclusions',
|
|
89
|
+
'__weakref__',
|
|
90
|
+
'_exclude_serialization',
|
|
91
|
+
'cid',
|
|
92
|
+
'config_module',
|
|
93
|
+
'connection_type',
|
|
94
|
+
'current_firm_version',
|
|
95
|
+
'device_image',
|
|
96
|
+
'device_name',
|
|
97
|
+
'device_region',
|
|
98
|
+
'device_type',
|
|
99
|
+
'enabled',
|
|
100
|
+
'features',
|
|
101
|
+
'last_response',
|
|
102
|
+
'latest_firm_version',
|
|
103
|
+
'mac_id',
|
|
104
|
+
'manager',
|
|
105
|
+
'pid',
|
|
106
|
+
'product_type',
|
|
107
|
+
'request_keys',
|
|
108
|
+
'state',
|
|
109
|
+
'sub_device_no',
|
|
110
|
+
'type',
|
|
111
|
+
'uuid',
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
state: VS_STATE_T
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
details: ResponseDeviceDetailsModel,
|
|
119
|
+
manager: VeSync,
|
|
120
|
+
feature_map: DeviceMapTemplate,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Initialize VeSync device base class."""
|
|
123
|
+
self._exclude_serialization: list[str] = []
|
|
124
|
+
self.enabled: bool = True
|
|
125
|
+
self.last_response: ResponseInfo | None = None
|
|
126
|
+
self.manager = manager
|
|
127
|
+
self.device_name: str = details.deviceName
|
|
128
|
+
self.device_image: str | None = details.deviceImg
|
|
129
|
+
self.cid: str = details.cid
|
|
130
|
+
self.connection_type: str | None = details.connectionType
|
|
131
|
+
self.device_type: str = details.deviceType
|
|
132
|
+
self.type: str | None = details.type
|
|
133
|
+
self.uuid: str | None = details.uuid
|
|
134
|
+
self.config_module: str = details.configModule
|
|
135
|
+
self.mac_id: str | None = details.macID
|
|
136
|
+
self.current_firm_version = details.currentFirmVersion
|
|
137
|
+
self.latest_firm_version: str | None = None
|
|
138
|
+
self.device_region: str | None = details.deviceRegion
|
|
139
|
+
self.pid: str | None = None
|
|
140
|
+
self.sub_device_no: int | None = details.subDeviceNo
|
|
141
|
+
# From feature_map
|
|
142
|
+
self.product_type: str = feature_map.product_type
|
|
143
|
+
self.features: list[str] = feature_map.features
|
|
144
|
+
|
|
145
|
+
def __eq__(self, other: object) -> bool:
|
|
146
|
+
"""Use device CID and sub-device number to test equality."""
|
|
147
|
+
if not isinstance(other, VeSyncBaseDevice):
|
|
148
|
+
return NotImplemented
|
|
149
|
+
return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no)
|
|
150
|
+
|
|
151
|
+
def __hash__(self) -> int:
|
|
152
|
+
"""Use CID and sub-device number to make device hash."""
|
|
153
|
+
return hash(self.cid + str(self.sub_device_no))
|
|
154
|
+
|
|
155
|
+
def __str__(self) -> str:
|
|
156
|
+
"""Use device info for string representation of class."""
|
|
157
|
+
return (
|
|
158
|
+
f'Device Name: {self.device_name}, '
|
|
159
|
+
f'Device Type: {self.device_type}, '
|
|
160
|
+
f'SubDevice No.: {self.sub_device_no}'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def __repr__(self) -> str:
|
|
164
|
+
"""Representation of device details."""
|
|
165
|
+
return (
|
|
166
|
+
f'DevClass: {self.__class__.__name__}, '
|
|
167
|
+
f'Product Type: {self.product_type}, '
|
|
168
|
+
f'Name:{self.device_name}, '
|
|
169
|
+
f'Device No: {self.sub_device_no}, '
|
|
170
|
+
f'CID: {self.cid}'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def is_on(self) -> bool:
|
|
175
|
+
"""Return true if device is on."""
|
|
176
|
+
return self.state.device_status in [DeviceStatus.ON, DeviceStatus.RUNNING]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def firmware_update(self) -> bool:
|
|
180
|
+
"""Return True if firmware update available.
|
|
181
|
+
|
|
182
|
+
This is going to be updated.
|
|
183
|
+
"""
|
|
184
|
+
return self.latest_firm_version != self.current_firm_version
|
|
185
|
+
|
|
186
|
+
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
187
|
+
"""Set timer for device.
|
|
188
|
+
|
|
189
|
+
This may not be implemented for all devices. Please open an issue
|
|
190
|
+
if there is an error.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
duration (int): Duration in seconds.
|
|
194
|
+
action (str | None): Action to take when timer expires.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
bool: True if successful, False otherwise.
|
|
198
|
+
"""
|
|
199
|
+
del duration
|
|
200
|
+
del action
|
|
201
|
+
logger.debug('Not implemented - set_timer')
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
async def get_timer(self) -> Timer | None:
|
|
205
|
+
"""Get timer for device from API and set the `state.Timer` attribute.
|
|
206
|
+
|
|
207
|
+
This may not be implemented for all devices. Please open an issue
|
|
208
|
+
if there is an error.
|
|
209
|
+
|
|
210
|
+
Note:
|
|
211
|
+
This method may not be implemented for all devices. Please
|
|
212
|
+
open an issue if there is an error.
|
|
213
|
+
"""
|
|
214
|
+
logger.debug('Not implemented - get_timer')
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
async def clear_timer(self) -> bool:
|
|
218
|
+
"""Clear timer for device from API.
|
|
219
|
+
|
|
220
|
+
This may not be implemented for all devices. Please open an issue
|
|
221
|
+
if there is an error.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
bool: True if successful, False otherwise.
|
|
225
|
+
"""
|
|
226
|
+
logger.debug('Not implemented - clear_timer')
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def set_state(self, state_attr: str, stat_value: Any) -> None: # noqa: ANN401
|
|
230
|
+
"""Set device state attribute."""
|
|
231
|
+
setattr(self, state_attr, stat_value)
|
|
232
|
+
|
|
233
|
+
def get_state(self, state_attr: str) -> Any: # noqa: ANN401
|
|
234
|
+
"""Get device state attribute."""
|
|
235
|
+
return getattr(self.state, state_attr)
|
|
236
|
+
|
|
237
|
+
@abstractmethod
|
|
238
|
+
async def get_details(self) -> None:
|
|
239
|
+
"""Get device details.
|
|
240
|
+
|
|
241
|
+
This method is defined in each device class to contain
|
|
242
|
+
the logic to pull the device state from the API and update
|
|
243
|
+
the device's `state` attribute. The `update()` method should
|
|
244
|
+
be called to update the device state.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
async def update(self) -> None:
|
|
248
|
+
"""Update device details."""
|
|
249
|
+
await self.get_details()
|
|
250
|
+
|
|
251
|
+
def display(self, state: bool = True) -> None:
|
|
252
|
+
"""Print formatted static device info to stdout.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
state (bool): If True, include state in display, defaults to True.
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
```
|
|
259
|
+
Device Name:..................Living Room Lamp
|
|
260
|
+
Model:........................ESL100
|
|
261
|
+
Subdevice No:.................0
|
|
262
|
+
Type:.........................wifi
|
|
263
|
+
CID:..........................1234567890abcdef
|
|
264
|
+
```
|
|
265
|
+
"""
|
|
266
|
+
# noinspection SpellCheckingInspection
|
|
267
|
+
display_list = [
|
|
268
|
+
('Device Name:', self.device_name),
|
|
269
|
+
('Product Type: ', self.product_type),
|
|
270
|
+
('Model: ', self.device_type),
|
|
271
|
+
('Subdevice No: ', str(self.sub_device_no)),
|
|
272
|
+
('Type: ', self.type),
|
|
273
|
+
('CID: ', self.cid),
|
|
274
|
+
('Config Module: ', self.config_module),
|
|
275
|
+
('Connection Type: ', self.connection_type),
|
|
276
|
+
('Features', self.features),
|
|
277
|
+
('Last Response: ', self.last_response),
|
|
278
|
+
]
|
|
279
|
+
if self.uuid is not None:
|
|
280
|
+
display_list.append(('UUID: ', self.uuid))
|
|
281
|
+
|
|
282
|
+
for line in display_list:
|
|
283
|
+
print(f'{line[0]:.<30} {line[1]}') # noqa: T201
|
|
284
|
+
if state:
|
|
285
|
+
self.state.display()
|
|
286
|
+
|
|
287
|
+
@deprecated('Use to_json() instead')
|
|
288
|
+
def displayJSON(
|
|
289
|
+
self, state: bool = True, indent: bool = True
|
|
290
|
+
) -> str: # pylint: disable=invalid-name
|
|
291
|
+
"""Return JSON details for device. - Deprecated use to_json()."""
|
|
292
|
+
return self.to_json(state, indent)
|
|
293
|
+
|
|
294
|
+
def to_json(self, state: bool = True, indent: bool = True) -> str:
|
|
295
|
+
"""Print JSON API string for device details.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
state (bool): If True, include state in JSON output, defaults to True.
|
|
299
|
+
indent (bool): If True, indent JSON output, defaults to True.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
str: JSON formatted string of device details.
|
|
303
|
+
"""
|
|
304
|
+
return self.to_jsonb(state, indent).decode()
|
|
305
|
+
|
|
306
|
+
def to_dict(self, state: bool = True) -> dict[str, Any]:
|
|
307
|
+
"""Return device information as a dictionary.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
state (bool): If True, include state in dictionary, defaults to True.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
dict[str, Any]: Dictionary containing device information.
|
|
314
|
+
"""
|
|
315
|
+
device_dict = {
|
|
316
|
+
'device_name': self.device_name,
|
|
317
|
+
'product_type': self.product_type,
|
|
318
|
+
'model': self.device_type,
|
|
319
|
+
'subdevice_no': str(self.sub_device_no),
|
|
320
|
+
'type': self.type,
|
|
321
|
+
'cid': self.cid,
|
|
322
|
+
'features:': self.features,
|
|
323
|
+
'config_module': self.config_module,
|
|
324
|
+
'connection_type': self.connection_type,
|
|
325
|
+
'last_response': self.last_response,
|
|
326
|
+
}
|
|
327
|
+
state_dict = self.state.to_dict() if state else {}
|
|
328
|
+
return device_dict | state_dict
|
|
329
|
+
|
|
330
|
+
def to_jsonb(self, state: bool = True, indent: bool = True) -> bytes:
|
|
331
|
+
"""JSON API bytes for device details.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
state (bool): If True, include state in JSON output, defaults to True.
|
|
335
|
+
indent (bool): If True, indent JSON output, defaults to True.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
bytes: JSON formatted bytes of device details.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
This is an example without state.
|
|
342
|
+
```
|
|
343
|
+
{
|
|
344
|
+
"Device Name": "Living Room Lamp",
|
|
345
|
+
"Model": "ESL100",
|
|
346
|
+
"Subdevice No": "0",
|
|
347
|
+
"Type": "wifi",
|
|
348
|
+
"CID": "1234567890abcdef"
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
"""
|
|
352
|
+
return_dict = self.to_dict(state=state)
|
|
353
|
+
if indent:
|
|
354
|
+
return orjson.dumps(
|
|
355
|
+
return_dict,
|
|
356
|
+
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return orjson.dumps(return_dict, option=orjson.OPT_NON_STR_KEYS)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class VeSyncBaseToggleDevice(VeSyncBaseDevice, Generic[VS_STATE_T]):
|
|
363
|
+
"""Base class for VeSync devices that can be toggled on and off.
|
|
364
|
+
|
|
365
|
+
Parameters:
|
|
366
|
+
details (ResponseDeviceDetailsModel): Device details from API call.
|
|
367
|
+
manager (VeSync): Manager object for API calls.
|
|
368
|
+
feature_map (DeviceMapTemplate): Device configuration map, will be specific
|
|
369
|
+
subclass of DeviceMapTemplate based on device type.
|
|
370
|
+
|
|
371
|
+
Attributes:
|
|
372
|
+
state (pyvesync.base_devices.vesyncbasedevice.DeviceState): Device state object
|
|
373
|
+
Each device has a separate state base class in the base_devices module.
|
|
374
|
+
last_response (ResponseInfo): Last response from API call.
|
|
375
|
+
manager (VeSync): Manager object for API calls.
|
|
376
|
+
device_name (str): Name of device.
|
|
377
|
+
device_image (str): URL for device image.
|
|
378
|
+
cid (str): Device ID.
|
|
379
|
+
connection_type (str): Connection type of device.
|
|
380
|
+
device_type (str): Type of device.
|
|
381
|
+
type (str): Type of device.
|
|
382
|
+
uuid (str): UUID of device, not always present.
|
|
383
|
+
config_module (str): Configuration module of device.
|
|
384
|
+
mac_id (str): MAC ID of device.
|
|
385
|
+
current_firm_version (str): Current firmware version of device.
|
|
386
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
387
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
388
|
+
sub_device_no (int): Sub-device number of device.
|
|
389
|
+
product_type (str): Product type of device.
|
|
390
|
+
features (dict): Features of device.
|
|
391
|
+
|
|
392
|
+
Methods:
|
|
393
|
+
set_timer: Set timer for device.
|
|
394
|
+
get_timer: Get timer for device from API.
|
|
395
|
+
clear_timer: Clear timer for device from API.
|
|
396
|
+
set_state: Set device state attribute.
|
|
397
|
+
get_state: Get device state attribute.
|
|
398
|
+
update: Update device details.
|
|
399
|
+
display: Print formatted static device info to stdout.
|
|
400
|
+
to_json: Print JSON API string
|
|
401
|
+
to_jsonb: JSON API bytes device details
|
|
402
|
+
to_dict: Return device information as a dictionary.
|
|
403
|
+
toggle_switch: Toggle device power on or off.
|
|
404
|
+
turn_on: Turn device on.
|
|
405
|
+
turn_off: Turn device off.
|
|
406
|
+
|
|
407
|
+
Note:
|
|
408
|
+
Device states are found in the `state` attribute in a subclass of DeviceState
|
|
409
|
+
based on the device type. The `DeviceState` subclass is located in device the
|
|
410
|
+
base_devices module.
|
|
411
|
+
|
|
412
|
+
The `last_response` attribute is used to store the last response and error
|
|
413
|
+
information from the API call. See the `pyvesync.errors` module for more
|
|
414
|
+
information.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
__slots__ = ()
|
|
418
|
+
|
|
419
|
+
@abstractmethod
|
|
420
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
421
|
+
"""Toggle device power on or off.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
toggle (bool | None): True to turn on, False to turn off, None to toggle.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
bool: True if successful, False otherwise.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
async def turn_on(self) -> bool:
|
|
431
|
+
"""Turn device on."""
|
|
432
|
+
return await self.toggle_switch(True)
|
|
433
|
+
|
|
434
|
+
async def turn_off(self) -> bool:
|
|
435
|
+
"""Turn device off."""
|
|
436
|
+
return await self.toggle_switch(False)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class DeviceState:
|
|
440
|
+
"""Base dataclass to hold device state.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
device (VeSyncBaseDevice): Device object.
|
|
444
|
+
details (ResponseDeviceDetailsModel): Device details from API call.
|
|
445
|
+
feature_map (DeviceMapTemplate): Device configuration map, will be specific
|
|
446
|
+
subclass of DeviceMapTemplate based on device type.
|
|
447
|
+
|
|
448
|
+
Attributes:
|
|
449
|
+
active_time (int): Active time of device, defaults to None.
|
|
450
|
+
connection_status (str): Connection status of device.
|
|
451
|
+
device (VeSyncBaseDevice): Device object.
|
|
452
|
+
device_status (str): Device status.
|
|
453
|
+
features (dict): Features of device.
|
|
454
|
+
last_update_ts (int): Last update timestamp in UTC, defaults to None.
|
|
455
|
+
|
|
456
|
+
Methods:
|
|
457
|
+
update_ts: Update last update timestamp.
|
|
458
|
+
to_dict: Dump state to JSON.
|
|
459
|
+
to_json: Dump state to JSON string.
|
|
460
|
+
to_jsonb: Dump state to JSON bytes.
|
|
461
|
+
as_tuple: Convert state to tuple of (name, value) tuples.
|
|
462
|
+
|
|
463
|
+
Note:
|
|
464
|
+
This cannot be instantiated directly. It should be inherited by the state class
|
|
465
|
+
of a specific product type.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
__slots__ = (
|
|
469
|
+
'__base_exclusions',
|
|
470
|
+
'_exclude_serialization',
|
|
471
|
+
'active_time',
|
|
472
|
+
'connection_status',
|
|
473
|
+
'device',
|
|
474
|
+
'device_status',
|
|
475
|
+
'features',
|
|
476
|
+
'last_update_ts',
|
|
477
|
+
'timer',
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
def __init__(
|
|
481
|
+
self,
|
|
482
|
+
device: VeSyncBaseDevice,
|
|
483
|
+
details: ResponseDeviceDetailsModel,
|
|
484
|
+
feature_map: DeviceMapTemplate,
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Initialize device state."""
|
|
487
|
+
self.__base_exclusions: list[str] = ['manager', 'device', 'state']
|
|
488
|
+
self._exclude_serialization: list[str] = []
|
|
489
|
+
self.device = device
|
|
490
|
+
self.device_status: str = details.deviceStatus or DeviceStatus.UNKNOWN
|
|
491
|
+
self.connection_status: str = details.connectionStatus or ConnectionStatus.UNKNOWN
|
|
492
|
+
self.features = feature_map.features
|
|
493
|
+
self.last_update_ts: int | None = None
|
|
494
|
+
self.active_time: int | None = None
|
|
495
|
+
self.timer: Timer | None = None
|
|
496
|
+
|
|
497
|
+
def __str__(self) -> str:
|
|
498
|
+
"""Return device state as string."""
|
|
499
|
+
return (
|
|
500
|
+
f'{self.device.device_name}, {self.device.product_type}, '
|
|
501
|
+
f'Device Status:{self.device_status}, '
|
|
502
|
+
f'Connection Status: {self.connection_status}'
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def __repr__(self) -> str:
|
|
506
|
+
"""Return device state as string."""
|
|
507
|
+
return (
|
|
508
|
+
f'{self.device.device_name}, Device Status:{self.device_status}, '
|
|
509
|
+
f'Connection Status: {self.connection_status}'
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def update_ts(self) -> None:
|
|
513
|
+
"""Update last update timestamp as UTC timestamp."""
|
|
514
|
+
self.last_update_ts = int(dt.now(tz=UTC).timestamp())
|
|
515
|
+
|
|
516
|
+
@staticmethod
|
|
517
|
+
def __predicate(attr: Any) -> bool: # noqa: ANN401
|
|
518
|
+
"""Check if attribute should be serialized."""
|
|
519
|
+
return (
|
|
520
|
+
callable(attr)
|
|
521
|
+
or inspect.ismethod(attr)
|
|
522
|
+
or inspect.isbuiltin(attr)
|
|
523
|
+
or inspect.isfunction(attr)
|
|
524
|
+
or inspect.isroutine(attr)
|
|
525
|
+
or inspect.isclass(attr)
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def _serialize(self) -> dict[str, Any]:
|
|
529
|
+
"""Get dictionary of state attributes."""
|
|
530
|
+
state_dict: dict[str, Any] = {}
|
|
531
|
+
for attr_name, attr_value in inspect.getmembers(
|
|
532
|
+
self, lambda a: not self.__predicate(a)
|
|
533
|
+
):
|
|
534
|
+
if attr_name.startswith('_') or attr_name in [
|
|
535
|
+
*self.__base_exclusions,
|
|
536
|
+
*self._exclude_serialization,
|
|
537
|
+
]:
|
|
538
|
+
continue
|
|
539
|
+
state_dict[attr_name] = attr_value
|
|
540
|
+
return state_dict
|
|
541
|
+
|
|
542
|
+
def to_json(self, indent: bool = False) -> str:
|
|
543
|
+
"""Dump state to JSON string.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
indent (bool): If True, indent JSON output, defaults to False.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
str: JSON formatted string of device state.
|
|
550
|
+
"""
|
|
551
|
+
return self.to_jsonb(indent=indent).decode()
|
|
552
|
+
|
|
553
|
+
def to_jsonb(self, indent: bool = False) -> bytes:
|
|
554
|
+
"""Convert state to JSON bytes."""
|
|
555
|
+
if indent:
|
|
556
|
+
return orjson.dumps(
|
|
557
|
+
self._serialize(), option=orjson.OPT_NON_STR_KEYS | orjson.OPT_INDENT_2
|
|
558
|
+
)
|
|
559
|
+
return orjson.dumps(self._serialize(), option=orjson.OPT_NON_STR_KEYS)
|
|
560
|
+
|
|
561
|
+
def to_dict(self) -> dict[str, Any]:
|
|
562
|
+
"""Convert state to dictionary."""
|
|
563
|
+
return self._serialize()
|
|
564
|
+
|
|
565
|
+
def as_tuple(self) -> tuple[tuple[str, Any], ...]:
|
|
566
|
+
"""Convert state to tuple of (name, value) tuples."""
|
|
567
|
+
return tuple((k, v) for k, v in self._serialize().items())
|
|
568
|
+
|
|
569
|
+
def display(self) -> None:
|
|
570
|
+
"""Print formatted state to stdout."""
|
|
571
|
+
for name, val in self._serialize().items():
|
|
572
|
+
print(f'{name:.<30} {val}') # noqa: T201
|