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