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.
Files changed (50) hide show
  1. pyvesync/__init__.py +5 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +330 -0
  4. pyvesync/base_devices/fan_base.py +392 -0
  5. pyvesync/base_devices/fryer_base.py +68 -0
  6. pyvesync/base_devices/humidifier_base.py +462 -0
  7. pyvesync/base_devices/outlet_base.py +327 -0
  8. pyvesync/base_devices/purifier_base.py +525 -0
  9. pyvesync/base_devices/switch_base.py +271 -0
  10. pyvesync/base_devices/thermostat_base.py +283 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +572 -0
  12. pyvesync/const.py +825 -0
  13. pyvesync/device_container.py +373 -0
  14. pyvesync/device_map.py +1172 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1126 -0
  17. pyvesync/devices/vesyncfan.py +233 -0
  18. pyvesync/devices/vesynchumidifier.py +808 -0
  19. pyvesync/devices/vesynckitchen.py +651 -0
  20. pyvesync/devices/vesyncoutlet.py +954 -0
  21. pyvesync/devices/vesyncpurifier.py +990 -0
  22. pyvesync/devices/vesyncswitch.py +415 -0
  23. pyvesync/devices/vesyncthermostat.py +171 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +129 -0
  26. pyvesync/models/bulb_models.py +197 -0
  27. pyvesync/models/bypass_models.py +289 -0
  28. pyvesync/models/fan_models.py +47 -0
  29. pyvesync/models/fryer_models.py +34 -0
  30. pyvesync/models/home_models.py +169 -0
  31. pyvesync/models/humidifier_models.py +202 -0
  32. pyvesync/models/outlet_models.py +220 -0
  33. pyvesync/models/purifier_models.py +261 -0
  34. pyvesync/models/switch_models.py +101 -0
  35. pyvesync/models/thermostat_models.py +134 -0
  36. pyvesync/models/vesync_models.py +439 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +250 -0
  39. pyvesync/utils/device_mixins.py +362 -0
  40. pyvesync/utils/enum_utils.py +26 -0
  41. pyvesync/utils/errors.py +867 -0
  42. pyvesync/utils/helpers.py +700 -0
  43. pyvesync/utils/logs.py +525 -0
  44. pyvesync/vesync.py +638 -0
  45. pyvesync/vesynchome.py +152 -0
  46. pyvesync-3.0.0.dist-info/METADATA +567 -0
  47. pyvesync-3.0.0.dist-info/RECORD +50 -0
  48. pyvesync-3.0.0.dist-info/WHEEL +5 -0
  49. pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
  50. pyvesync-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,373 @@
1
+ """Module to contain VeSync device instances.
2
+
3
+ Attributes:
4
+ DeviceContainerInstance (DeviceContainer): Singleton instance of the DeviceContainer
5
+ class. This is imported by the `vesync` module.
6
+
7
+ Classes:
8
+ DeviceContainer: Container for VeSync device instances.
9
+ This class should not be instantiated directly. Use the `DeviceContainerInstance`
10
+ instead.
11
+ _DeviceContainerBase: Base class for VeSync device
12
+ container. Inherits from `MutableSet`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import re
19
+ from collections.abc import Iterator, MutableSet, Sequence
20
+ from typing import TYPE_CHECKING, TypeVar
21
+
22
+ from pyvesync.base_devices.bulb_base import VeSyncBulb
23
+ from pyvesync.base_devices.fan_base import VeSyncFanBase
24
+ from pyvesync.base_devices.fryer_base import VeSyncFryer
25
+ from pyvesync.base_devices.humidifier_base import VeSyncHumidifier
26
+ from pyvesync.base_devices.outlet_base import VeSyncOutlet
27
+ from pyvesync.base_devices.purifier_base import VeSyncPurifier
28
+ from pyvesync.base_devices.switch_base import VeSyncSwitch
29
+ from pyvesync.base_devices.thermostat_base import VeSyncThermostat
30
+ from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice
31
+ from pyvesync.const import ProductTypes
32
+ from pyvesync.device_map import get_device_config
33
+
34
+ if TYPE_CHECKING:
35
+ from pyvesync import VeSync
36
+ from pyvesync.models.vesync_models import (
37
+ ResponseDeviceDetailsModel,
38
+ ResponseDeviceListModel,
39
+ )
40
+
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ T = TypeVar('T')
45
+
46
+
47
+ def _clean_string(string: str) -> str:
48
+ """Clean a string by removing non alphanumeric characters and making lowercase."""
49
+ return re.sub(r'[^a0zA-Z0-9]', '', string)
50
+
51
+
52
+ class _DeviceContainerBase(MutableSet[VeSyncBaseDevice]):
53
+ """Base class for VeSync device container.
54
+
55
+ Inherits from `MutableSet` and defines the core MutableSet methods.
56
+ """
57
+
58
+ __slots__ = ('__weakref__', '_data')
59
+
60
+ def __init__(
61
+ self,
62
+ sequence: Sequence[VeSyncBaseDevice] | None = None,
63
+ /,
64
+ ) -> None:
65
+ """Initialize the DeviceContainer class."""
66
+ self._data: set[VeSyncBaseDevice] = set()
67
+ if isinstance(sequence, Sequence):
68
+ self._data.update(sequence)
69
+
70
+ def __iter__(self) -> Iterator[VeSyncBaseDevice]:
71
+ """Iterate over the container."""
72
+ return iter(self._data)
73
+
74
+ def __len__(self) -> int:
75
+ """Return the length of the container."""
76
+ return len(self._data)
77
+
78
+ def add(self, value: VeSyncBaseDevice) -> None:
79
+ """Add a device to the container."""
80
+ if value in self._data:
81
+ logger.debug('Device already exists')
82
+ return
83
+ self._data.add(value)
84
+
85
+ def remove(self, value: VeSyncBaseDevice) -> None:
86
+ """Remove a device from the container."""
87
+ self._data.remove(value)
88
+
89
+ def clear(self) -> None:
90
+ """Clear the container."""
91
+ self._data.clear()
92
+
93
+ def __contains__(self, value: object) -> bool:
94
+ """Check if a device is in the container."""
95
+ return value in self._data
96
+
97
+
98
+ class DeviceContainer(_DeviceContainerBase):
99
+ """Container for VeSync device instances.
100
+
101
+ /// admonition | Warning
102
+ type: warning
103
+
104
+ This class should not be instantiated directly. Use the `DeviceContainerInstance`
105
+ instead.
106
+ ///
107
+
108
+ The `DeviceContainer` class is a container for VeSync device instances that
109
+ inherits behavior from `MutableSet`. The container contains all VeSync devices
110
+ and is used to store and manage device instances. The container is a singleton
111
+ and is instantiated directly by the `DeviceContainerInstance` in the
112
+ `device_container` module and imported as needed.
113
+
114
+ Use the [`add_new_devices`][pyvesync.device_container.DeviceContainer.add_new_devices]
115
+ class method to add devices from the device list model API response and
116
+ `remove_stale_devices` to remove stale devices from the device list API response
117
+ model. The device list response model is built in the
118
+ [VeSync.get_devices()][pyvesync.vesync.VeSync.get_devices] method.
119
+
120
+ Args:
121
+ sequence (Sequence[VeSyncBaseDevice] | None): A sequence of device instances to
122
+ initialize the container with. Typically this is not used directly,
123
+ defaults to None.
124
+
125
+ Attributes:
126
+ _data (set[VeSyncBaseDevice]): The mutable set of devices in the container.
127
+ """
128
+
129
+ __slots__ = ()
130
+
131
+ def __init__(
132
+ self,
133
+ sequence: Sequence[VeSyncBaseDevice] | None = None,
134
+ /,
135
+ ) -> None:
136
+ """Initialize the DeviceContainer class."""
137
+ super().__init__(sequence)
138
+
139
+ def _build_device_instance(
140
+ self, device: ResponseDeviceDetailsModel, manager: VeSync
141
+ ) -> VeSyncBaseDevice | None:
142
+ """Create a device from a single device model from the device list.
143
+
144
+ Args:
145
+ device (ResponseDeviceDetailsModel): The device details model from the
146
+ device list response model.
147
+ manager (VeSync): The VeSync instance to pass to the device instance
148
+
149
+ Returns:
150
+ VeSyncBaseDevice: The device instance created from the device list
151
+ response model.
152
+
153
+ Raises:
154
+ VeSyncAPIResponseError: If the model is not an instance of
155
+ `ResponseDeviceDetailsModel`.
156
+ """
157
+ device_features = get_device_config(device.deviceType)
158
+ if device_features is None:
159
+ logger.debug('Device type %s not found in device map', device.deviceType)
160
+ return None
161
+ dev_class = device_features.class_name
162
+ dev_module = device_features.module
163
+ device.productType = device_features.product_type
164
+ # Import via string to avoid circular imports
165
+ cls = getattr(dev_module, dev_class)
166
+ return cls(device, manager, device_features)
167
+
168
+ def add_device_from_model(
169
+ self, device: ResponseDeviceDetailsModel, manager: VeSync
170
+ ) -> None:
171
+ """Add a single device from the device list response model.
172
+
173
+ Args:
174
+ device (ResponseDeviceDetailsModel): The device details model from the
175
+ device list response model.
176
+ manager (VeSync): The VeSync instance to pass to the device instance
177
+
178
+ Raises:
179
+ VeSyncAPIResponseError: If the model is not an instance of
180
+ `ResponseDeviceDetailsModel`.
181
+ """
182
+ device_obj = self._build_device_instance(device, manager)
183
+ if device_obj is not None:
184
+ self.add(device_obj)
185
+
186
+ def device_exists(self, cid: str, sub_device_no: int | None = None) -> bool:
187
+ """Check if a device with the given cid & sub_dev_no exists.
188
+
189
+ Args:
190
+ cid (str): The cid of the device to check.
191
+ sub_device_no (int): The sub_device_no of the device to check, defaults to 0
192
+ for most devices.
193
+
194
+ Returns:
195
+ bool: True if the device exists, False otherwise.
196
+ """
197
+ new_hash = hash(cid + str(sub_device_no))
198
+ return any(new_hash == hash(dev) for dev in self._data)
199
+
200
+ def get_by_name(self, name: str, fuzzy: bool = False) -> VeSyncBaseDevice | None:
201
+ """Forgiving method to get a device by name.
202
+
203
+ Args:
204
+ name (str): The name of the device to get.
205
+ fuzzy (bool): Use a fuzzy match to find the device. Defaults to False.
206
+
207
+ Returns:
208
+ VeSyncBaseDevice | None: The device instance if found, None otherwise.
209
+
210
+ Note:
211
+ Fuzzy matching removes all non-alphanumeric characters and makes the string
212
+ lowercase. If there are multiple devices with the same name, the first one
213
+ found will be returned (a set is unordered).
214
+ """
215
+ for device in self._data:
216
+ if (fuzzy and _clean_string(device.device_name) == _clean_string(name)) or (
217
+ device.device_name == name
218
+ ):
219
+ return device
220
+ return None
221
+
222
+ def remove_by_cid(self, cid: str) -> bool:
223
+ """Remove a device by cid.
224
+
225
+ Args:
226
+ cid (str): The cid of the device to remove.
227
+
228
+ Returns:
229
+ bool : True if the device was removed, False otherwise.
230
+ """
231
+ device_found: VeSyncBaseDevice | None = None
232
+ for device in self._data:
233
+ if device.cid == cid:
234
+ device_found = device
235
+ break
236
+ if device_found is not None:
237
+ self.remove(device_found)
238
+ return True
239
+ return False
240
+
241
+ def discard(self, value: VeSyncBaseDevice) -> None:
242
+ """Discard a device from the container.
243
+
244
+ Args:
245
+ value (VeSyncBaseDevice): The device to discard.
246
+ """
247
+ return self._data.discard(value)
248
+
249
+ def remove_stale_devices(self, device_list_result: ResponseDeviceListModel) -> None:
250
+ """Remove devices that are not in the provided list.
251
+
252
+ Args:
253
+ device_list_result (ResponseDeviceListModel): The device list response model
254
+ from the VeSync API. This is generated by the `VeSync.get_devices()`
255
+ method.
256
+ """
257
+ device_list = device_list_result.result.list
258
+ new_hashes = [
259
+ hash(device.cid + str(device.subDeviceNo)) for device in device_list
260
+ ]
261
+ remove_cids = []
262
+ for device in self._data:
263
+ if hash(device) not in new_hashes:
264
+ logger.debug('Removing stale device %s', device.device_name)
265
+ remove_cids.append(device.cid)
266
+ for cid in remove_cids:
267
+ self.remove_by_cid(cid)
268
+
269
+ def add_new_devices(
270
+ self, device_list_result: ResponseDeviceListModel, manager: VeSync
271
+ ) -> None:
272
+ """Add new devices to the container.
273
+
274
+ Args:
275
+ device_list_result (ResponseDeviceListModel): The device list response model
276
+ from the VeSync API. This is generated by the `VeSync.get_devices()`
277
+ method.
278
+ manager (VeSync): The VeSync instance to pass to the device instance
279
+ """
280
+ device_list = device_list_result.result.list
281
+ for device in device_list:
282
+ if self.device_exists(device.cid, device.subDeviceNo) not in self._data:
283
+ self.add_device_from_model(device, manager)
284
+
285
+ @property
286
+ def outlets(self) -> list[VeSyncOutlet]:
287
+ """Return a list of devices that are outlets."""
288
+ return [
289
+ device
290
+ for device in self
291
+ if isinstance(device, VeSyncOutlet)
292
+ and device.product_type == ProductTypes.OUTLET
293
+ ]
294
+
295
+ @property
296
+ def switches(self) -> list[VeSyncSwitch]:
297
+ """Return a list of devices that are switches."""
298
+ return [
299
+ device
300
+ for device in self
301
+ if (
302
+ isinstance(device, VeSyncSwitch)
303
+ and device.product_type == ProductTypes.SWITCH
304
+ )
305
+ ]
306
+
307
+ @property
308
+ def bulbs(self) -> list[VeSyncBulb]:
309
+ """Return a list of devices that are lights."""
310
+ return [
311
+ device
312
+ for device in self
313
+ if isinstance(device, VeSyncBulb)
314
+ and (device.product_type == ProductTypes.BULB)
315
+ ]
316
+
317
+ @property
318
+ def air_purifiers(self) -> list[VeSyncPurifier]:
319
+ """Return a list of devices that are air purifiers."""
320
+ return [
321
+ device
322
+ for device in self
323
+ if isinstance(device, VeSyncPurifier)
324
+ and device.product_type == ProductTypes.PURIFIER
325
+ ]
326
+
327
+ @property
328
+ def fans(self) -> list[VeSyncFanBase]:
329
+ """Return a list of devices that are fans."""
330
+ return [
331
+ device
332
+ for device in self
333
+ if isinstance(device, VeSyncFanBase)
334
+ and device.product_type == ProductTypes.FAN
335
+ ]
336
+
337
+ @property
338
+ def humidifiers(self) -> list[VeSyncHumidifier]:
339
+ """Return a list of devices that are humidifiers."""
340
+ return [
341
+ device
342
+ for device in self
343
+ if isinstance(device, VeSyncHumidifier)
344
+ and device.product_type == ProductTypes.HUMIDIFIER
345
+ ]
346
+
347
+ @property
348
+ def air_fryers(self) -> list[VeSyncFryer]:
349
+ """Return a list of devices that are air fryers."""
350
+ return [
351
+ device
352
+ for device in self
353
+ if isinstance(device, VeSyncFryer)
354
+ and device.product_type == ProductTypes.AIR_FRYER
355
+ ]
356
+
357
+ @property
358
+ def thermostats(self) -> list[VeSyncThermostat]:
359
+ """Return a list of devices that are thermostats."""
360
+ return [
361
+ device
362
+ for device in self
363
+ if isinstance(device, VeSyncThermostat)
364
+ and device.product_type == ProductTypes.THERMOSTAT
365
+ ]
366
+
367
+
368
+ DeviceContainerInstance = DeviceContainer()
369
+ """Singleton instance of the DeviceContainer class.
370
+
371
+ This attribute should be imported by the `vesync` module and
372
+ not the `DeviceContainer` class directly.
373
+ """