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,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