pywemo 1.4.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 (40) hide show
  1. pywemo/README.md +69 -0
  2. pywemo/__init__.py +33 -0
  3. pywemo/color.py +79 -0
  4. pywemo/discovery.py +194 -0
  5. pywemo/exceptions.py +94 -0
  6. pywemo/ouimeaux_device/LICENSE +12 -0
  7. pywemo/ouimeaux_device/__init__.py +679 -0
  8. pywemo/ouimeaux_device/api/__init__.py +1 -0
  9. pywemo/ouimeaux_device/api/attributes.py +131 -0
  10. pywemo/ouimeaux_device/api/db_orm.py +197 -0
  11. pywemo/ouimeaux_device/api/long_press.py +168 -0
  12. pywemo/ouimeaux_device/api/rules_db.py +467 -0
  13. pywemo/ouimeaux_device/api/service.py +363 -0
  14. pywemo/ouimeaux_device/api/wemo_services.py +25 -0
  15. pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
  16. pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
  17. pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
  18. pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
  19. pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
  20. pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
  21. pywemo/ouimeaux_device/api/xsd_types.py +222 -0
  22. pywemo/ouimeaux_device/bridge.py +506 -0
  23. pywemo/ouimeaux_device/coffeemaker.py +92 -0
  24. pywemo/ouimeaux_device/crockpot.py +157 -0
  25. pywemo/ouimeaux_device/dimmer.py +70 -0
  26. pywemo/ouimeaux_device/humidifier.py +223 -0
  27. pywemo/ouimeaux_device/insight.py +191 -0
  28. pywemo/ouimeaux_device/lightswitch.py +11 -0
  29. pywemo/ouimeaux_device/maker.py +54 -0
  30. pywemo/ouimeaux_device/motion.py +6 -0
  31. pywemo/ouimeaux_device/outdoor_plug.py +6 -0
  32. pywemo/ouimeaux_device/switch.py +32 -0
  33. pywemo/py.typed +0 -0
  34. pywemo/ssdp.py +372 -0
  35. pywemo/subscribe.py +782 -0
  36. pywemo/util.py +139 -0
  37. pywemo-1.4.0.dist-info/LICENSE +54 -0
  38. pywemo-1.4.0.dist-info/METADATA +192 -0
  39. pywemo-1.4.0.dist-info/RECORD +40 -0
  40. pywemo-1.4.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,506 @@
1
+ """Representation of a WeMo Bridge (Link) device."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import time
6
+ import warnings
7
+ from html import escape
8
+ from typing import Any, Iterable, TypedDict
9
+
10
+ from lxml import etree as et
11
+
12
+ from ..color import ColorXY, get_profiles, limit_to_gamut
13
+ from ..exceptions import InvalidSchemaError
14
+ from . import Device
15
+ from .api.service import RequiredService
16
+
17
+ CAPABILITY_ID2NAME = dict(
18
+ (
19
+ ("10006", "onoff"),
20
+ ("10008", "levelcontrol"),
21
+ ("30008", "sleepfader"),
22
+ ("30009", "levelcontrol_move"),
23
+ ("3000A", "levelcontrol_stop"),
24
+ ("10300", "colorcontrol"),
25
+ ("30301", "colortemperature"),
26
+ )
27
+ )
28
+ CAPABILITY_NAME2ID = dict(
29
+ (val, cap) for cap, val in CAPABILITY_ID2NAME.items()
30
+ )
31
+
32
+ # acceptable values for 'onoff'
33
+ OFF = 0
34
+ ON = 1
35
+ TOGGLE = 2
36
+
37
+
38
+ def _warn_rename(old: str, new: str, stacklevel: int = 3) -> None:
39
+ warnings.warn(
40
+ f"{old} is deprecated and will be removed in a future release. "
41
+ f"Use {new} instead",
42
+ DeprecationWarning,
43
+ stacklevel=stacklevel,
44
+ )
45
+
46
+
47
+ def limit(value: int, min_val: int, max_val: int) -> int:
48
+ """Return a value clipped to the range [min_val, max_val]."""
49
+ return max(min_val, min(value, max_val))
50
+
51
+
52
+ class Bridge(Device):
53
+ """Representation of a WeMo Bridge (Link) device."""
54
+
55
+ lights: dict[str, Light]
56
+ groups: dict[str, Group]
57
+
58
+ EVENT_TYPE_STATUS_CHANGE = "StatusChange"
59
+
60
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
61
+ """Create a WeMo Bridge (Link) device."""
62
+ super().__init__(*args, **kwargs)
63
+ self.lights = {}
64
+ self.groups = {}
65
+ self.bridge_update()
66
+
67
+ def __repr__(self) -> str:
68
+ """Return a string representation of the device."""
69
+ return (
70
+ '<WeMo Bridge "{name}", Lights: {lights}, ' + "Groups: {groups}>"
71
+ ).format(
72
+ name=self.name, lights=len(self.lights), groups=len(self.groups)
73
+ )
74
+
75
+ @property
76
+ def _required_services(self) -> list[RequiredService]:
77
+ return super()._required_services + [
78
+ RequiredService(name="basicevent", actions=["GetMacAddr"]),
79
+ RequiredService(
80
+ name="bridge",
81
+ actions=["GetDeviceStatus", "SetDeviceStatus"],
82
+ ),
83
+ ]
84
+
85
+ @property
86
+ def Lights(self) -> dict[str, Light]: # pylint: disable=invalid-name
87
+ """Deprecated method for accessing .lights."""
88
+ _warn_rename("Lights", "lights")
89
+ return self.lights
90
+
91
+ @property
92
+ def Groups(self) -> dict[str, Group]: # pylint: disable=invalid-name
93
+ """Deprecated method for accessing .groups."""
94
+ _warn_rename("Groups", "groups")
95
+ return self.groups
96
+
97
+ def bridge_update(
98
+ self, force_update: bool = True
99
+ ) -> tuple[dict[str, Light], dict[str, Group]]:
100
+ """Get updated status information for the bridge and its lights."""
101
+ if force_update or self.lights is None or self.groups is None:
102
+ plugin_udn = self.basicevent.GetMacAddr().get("PluginUDN")
103
+
104
+ if hasattr(self.bridge, "GetEndDevicesWithStatus"):
105
+ end_devices = self.bridge.GetEndDevicesWithStatus(
106
+ DevUDN=plugin_udn, ReqListType="PAIRED_LIST"
107
+ )
108
+ else:
109
+ end_devices = self.bridge.GetEndDevices(
110
+ DevUDN=plugin_udn, ReqListType="PAIRED_LIST"
111
+ )
112
+
113
+ if not (end_devices_xml := end_devices.get("DeviceLists")):
114
+ return self.lights, self.groups
115
+
116
+ end_device_list = et.fromstring(
117
+ end_devices_xml.encode("utf-8"),
118
+ parser=et.XMLParser(resolve_entities=False),
119
+ )
120
+
121
+ for light in end_device_list.iter("DeviceInfo"):
122
+ if not (device_id := light.findtext("DeviceID")):
123
+ raise InvalidSchemaError(
124
+ f"DeviceID missing: {et.tostring(light).decode()}"
125
+ )
126
+ if device_id in self.lights:
127
+ self.lights[device_id].update_state(light)
128
+ else:
129
+ self.lights[device_id] = Light(self, light)
130
+
131
+ for group in end_device_list.iter("GroupInfo"):
132
+ if not (group_id := group.findtext("GroupID")):
133
+ raise InvalidSchemaError(
134
+ f"GroupID missing: {et.tostring(group).decode()}"
135
+ )
136
+ if group_id in self.groups:
137
+ self.groups[group_id].update_state(group)
138
+ else:
139
+ self.groups[group_id] = Group(self, group)
140
+
141
+ return self.lights, self.groups
142
+
143
+ def get_state(self, force_update: bool = False) -> int:
144
+ """Update the state of the Bridge device."""
145
+ state = super().get_state(force_update)
146
+ self.bridge_update(force_update)
147
+ return state
148
+
149
+ def subscription_update(self, _type: str, _param: str) -> bool:
150
+ """Update the bridge attributes due to a subscription update event."""
151
+ if _type == self.EVENT_TYPE_STATUS_CHANGE and _param:
152
+ try:
153
+ state_event = et.fromstring(
154
+ _param.encode("utf8"),
155
+ parser=et.XMLParser(resolve_entities=False),
156
+ )
157
+ except et.XMLSyntaxError:
158
+ return False
159
+ if not (key := state_event.findtext("DeviceID")):
160
+ return False
161
+ if key in self.lights:
162
+ return self.lights[key].subscription_update(state_event)
163
+
164
+ if key in self.groups:
165
+ return self.groups[key].subscription_update(state_event)
166
+
167
+ return False
168
+ return super().subscription_update(_type, _param)
169
+
170
+ def bridge_getdevicestatus(self, deviceid: str) -> et._Element | None:
171
+ """Return the list of device statuses for the bridge's lights."""
172
+ status_list = self.bridge.GetDeviceStatus(DeviceIDs=deviceid)
173
+ device_status_list_xml = status_list.get("DeviceStatusList")
174
+ if not device_status_list_xml:
175
+ return None
176
+ device_status_list = et.fromstring(
177
+ device_status_list_xml.encode("utf-8"),
178
+ parser=et.XMLParser(resolve_entities=False),
179
+ )
180
+
181
+ return device_status_list.find("DeviceStatus")
182
+
183
+ def bridge_setdevicestatus(
184
+ self, isgroup: str, deviceid: str, capids: list[str], values: list[str]
185
+ ) -> dict[str, str]:
186
+ """Set the status of the bridge's lights."""
187
+ req = et.Element("DeviceStatus")
188
+ et.SubElement(req, "IsGroupAction").text = isgroup
189
+ et.SubElement(req, "DeviceID", available="YES").text = deviceid
190
+ et.SubElement(req, "CapabilityID").text = ",".join(capids)
191
+ et.SubElement(req, "CapabilityValue").text = ",".join(values)
192
+
193
+ buf = io.BytesIO()
194
+ et.ElementTree(req).write(buf, encoding="UTF-8", xml_declaration=True)
195
+ send_state = escape(buf.getvalue().decode(), quote=True)
196
+
197
+ return self.bridge.SetDeviceStatus(DeviceStatusList=send_state)
198
+
199
+
200
+ class DeviceState(TypedDict, total=False):
201
+ """LinkedDevice state dictionary type."""
202
+
203
+ available: bool
204
+ onoff: int
205
+ level: int
206
+ temperature_mireds: int
207
+ temperature_kelvin: int
208
+ color_xy: ColorXY
209
+
210
+
211
+ class LinkedDevice: # pylint: disable=too-many-instance-attributes
212
+ """Representation of a device connected to the bridge."""
213
+
214
+ _NAME_TAG: str
215
+ _CAPABILITIES_TAGS: tuple[str, ...]
216
+ _VALUES_TAGS: tuple[str, ...]
217
+
218
+ def __init__(self, bridge: Bridge, info: et._Element) -> None:
219
+ """Create a Linked Device."""
220
+ self.bridge: Bridge = bridge
221
+ self.host: str = self.bridge.host
222
+ self.port: int = self.bridge.port
223
+ self.name: str = ""
224
+ self.state: DeviceState = {}
225
+ self.capabilities: Iterable[str] = tuple()
226
+ self.update_state(info)
227
+ self._last_err: dict[str, str] = {}
228
+ self.mac: str = self.bridge.mac
229
+ self.serial_number: str = self.bridge.serial_number
230
+ self.uniqueID: str = "" # pylint: disable=invalid-name
231
+
232
+ def get_state(self, force_update: bool = False) -> DeviceState:
233
+ """Return the status of the device."""
234
+ if force_update:
235
+ self.bridge.bridge_update()
236
+ return self.state
237
+
238
+ def update_state(self, status: et._Element) -> None:
239
+ """Fetch the capabilities and values then update the device state."""
240
+ if name := status.findtext(self._NAME_TAG, ""):
241
+ self.name = name
242
+
243
+ def get_first_text(tags: Iterable[str]) -> str | None:
244
+ candidates = (status.findtext(tag) for tag in tags)
245
+ return next(filter(bool, candidates), None)
246
+
247
+ if capabilities := get_first_text(self._CAPABILITIES_TAGS):
248
+ self.capabilities = tuple(
249
+ CAPABILITY_ID2NAME.get(c, c) for c in capabilities.split(",")
250
+ )
251
+ if current_state := get_first_text(self._VALUES_TAGS):
252
+ self._update_values(
253
+ zip(self.capabilities, current_state.split(","))
254
+ )
255
+
256
+ def _update_values(self, values: Iterable[tuple[str, str]]) -> None:
257
+ """Set the device state based on capabilities and values."""
258
+ status: dict[str, tuple[int, ...] | None] = {}
259
+ for capability, value in values:
260
+ if capability not in CAPABILITY_NAME2ID:
261
+ continue # Ignore unsupported capabilities.
262
+ if not value:
263
+ status[capability] = None
264
+ continue
265
+ try:
266
+ status[capability] = tuple(
267
+ int(round(float(v))) for v in value.split(":")
268
+ )
269
+ except ValueError as err:
270
+ raise ValueError(
271
+ f"Invalid value for {capability}: {repr(value)}"
272
+ ) from err
273
+
274
+ # unreachable devices have empty strings for all capability values
275
+ if (on_off := status.get("onoff", ("Missing",))) is None:
276
+ self.state["available"] = False
277
+ self.state["onoff"] = 0
278
+ elif isinstance(on_off[0], int):
279
+ self.state["available"] = True
280
+ self.state["onoff"] = on_off[0]
281
+
282
+ if (level_control := status.get("levelcontrol")) is not None:
283
+ self.state["level"] = level_control[0]
284
+
285
+ if (color_temperature := status.get("colortemperature")) is not None:
286
+ temperature = color_temperature[0]
287
+ if temperature <= 0:
288
+ raise ValueError(
289
+ f"Invalid value for color temperature: {temperature}"
290
+ )
291
+ self.state["temperature_mireds"] = temperature
292
+ self.state["temperature_kelvin"] = int(1000000 / temperature)
293
+
294
+ if (color_control := status.get("colorcontrol")) is not None:
295
+ if len(color_control) < 2:
296
+ raise ValueError(
297
+ f"Too few values for colorcontrol: {repr(color_control)}"
298
+ )
299
+ color_x, color_y = float(color_control[0]), float(color_control[1])
300
+ color_x, color_y = color_x / 65535.0, color_y / 65535.0
301
+ self.state["color_xy"] = color_x, color_y
302
+
303
+ def subscription_update(self, state_event: et._Element) -> bool:
304
+ """Update the light values due to a subscription update event."""
305
+ if (
306
+ device_id := state_event.find("DeviceID")
307
+ ) is None or device_id.get("available", "YES").upper() == "YES":
308
+ capability = state_event.findtext("CapabilityId")
309
+ value = state_event.findtext("Value")
310
+ else:
311
+ capability = CAPABILITY_NAME2ID.get("onoff")
312
+ value = "" # Use an empty string to indicate an unreachable device
313
+
314
+ if capability is None or value is None:
315
+ return False
316
+
317
+ name = CAPABILITY_ID2NAME.get(capability, capability)
318
+ if name not in self.capabilities:
319
+ # Should't receive updates for capabilities that were not
320
+ # originally present.
321
+ return False
322
+
323
+ try:
324
+ self._update_values([(name, value)])
325
+ except ValueError:
326
+ return False
327
+ return True
328
+
329
+ def _setdevicestatus(self, **kwargs: Any) -> LinkedDevice:
330
+ """Ask the bridge to set the device status."""
331
+ isgroup = "YES" if isinstance(self, Group) else "NO"
332
+
333
+ capids = []
334
+ values = []
335
+ for cap, val in kwargs.items():
336
+ capids.append(CAPABILITY_NAME2ID[cap])
337
+
338
+ if not isinstance(val, (list, tuple)):
339
+ val = (val,)
340
+ values.append(":".join(str(v) for v in val))
341
+
342
+ self._last_err = self.bridge.bridge_setdevicestatus(
343
+ isgroup, self.uniqueID, capids, values
344
+ )
345
+ return self
346
+
347
+ def turn_on( # pylint: disable=unused-argument
348
+ self,
349
+ level: int | None = None,
350
+ transition: int = 0,
351
+ force_update: bool = False,
352
+ ) -> LinkedDevice:
353
+ """Turn on the device."""
354
+ return self._setdevicestatus(onoff=ON)
355
+
356
+ def turn_off( # pylint: disable=unused-argument
357
+ self, transition: int = 0
358
+ ) -> LinkedDevice:
359
+ """Turn off the device."""
360
+ return self._setdevicestatus(onoff=OFF)
361
+
362
+ def toggle(self) -> LinkedDevice:
363
+ """Toggle the device from on to off or off to on."""
364
+ return self._setdevicestatus(onoff=TOGGLE)
365
+
366
+ @property
367
+ def device_type(self) -> str:
368
+ """Return what kind of WeMo this device is."""
369
+ return type(self).__name__
370
+
371
+ def __repr__(self) -> str:
372
+ """Return a string representation of the device."""
373
+ return f'<{self.device_type.upper()} "{self.name}">'
374
+
375
+
376
+ class Light(LinkedDevice): # pylint: disable=too-many-instance-attributes
377
+ """Representation of a Light connected to the Bridge."""
378
+
379
+ _NAME_TAG = "FriendlyName"
380
+ _CAPABILITIES_TAGS = ("CapabilityIDs", "CapabilityID")
381
+ _VALUES_TAGS = ("CurrentState", "CapabilityValue")
382
+
383
+ def __init__(self, bridge: Bridge, info: et._Element) -> None:
384
+ """Create a Light device."""
385
+ super().__init__(bridge, info)
386
+
387
+ self.device_index: str = info.findtext("DeviceIndex", "")
388
+ self.uniqueID: str = info.findtext("DeviceID", "")
389
+ self.iconvalue: str = info.findtext("IconVersion", "")
390
+ self.firmware: str = info.findtext("FirmwareVersion", "")
391
+ self.manufacturer: str = info.findtext("Manufacturer", "")
392
+ self.model: str = info.findtext("ModelCode", "")
393
+ self.certified: str = info.findtext("WeMoCertified", "")
394
+
395
+ self.temperature_range, self.gamut = get_profiles(self.model)
396
+ self._pending: dict[str, Any] = {}
397
+
398
+ def _queuedevicestatus(self, queue: bool = False, **kwargs: Any) -> Light:
399
+ """Queue an update to the device."""
400
+ if kwargs:
401
+ self._pending.update(kwargs)
402
+ if not queue:
403
+ self._setdevicestatus(**self._pending)
404
+ self._pending = {}
405
+
406
+ return self
407
+
408
+ def turn_on(
409
+ self,
410
+ level: int | None = None,
411
+ transition: int = 0,
412
+ force_update: bool = False,
413
+ ) -> Light:
414
+ """Turn on the light."""
415
+ transition_time = limit(int(transition * 10), 0, 65535)
416
+
417
+ if level == 0:
418
+ return self.turn_off(transition)
419
+ if "levelcontrol" in self.capabilities:
420
+ # Work around observed fw bugs.
421
+ # - When we set a new brightness level but the bulb is off, it
422
+ # first turns on at the old brightness and then fades to the new
423
+ # setting. So we have to force the saved brightness to 0 first.
424
+ # - When we turn a bulb on with levelcontrol the onoff state
425
+ # doesn't update.
426
+ # - After turning off a bulb with sleepfader, it fails to turn back
427
+ # on unless the brightness is re-set with levelcontrol.
428
+ self.get_state(force_update=force_update)
429
+ # A freshly power cycled bridge has no record of the bulb
430
+ # brightness, so default to full on if the client didn't request
431
+ # a level and we have no record
432
+ if level is None:
433
+ level = self.state.get("level", 255)
434
+
435
+ if self.state["onoff"] == 0:
436
+ self._setdevicestatus(levelcontrol=(0, 0), onoff=ON)
437
+
438
+ level = limit(int(level), 0, 255)
439
+ return self._queuedevicestatus(
440
+ levelcontrol=(level, transition_time)
441
+ )
442
+
443
+ return self._queuedevicestatus(onoff=ON)
444
+
445
+ def turn_off(self, transition: int = 0) -> Light:
446
+ """Turn off the light."""
447
+ if transition and "sleepfader" in self.capabilities:
448
+ # Sleepfader control did not turn off bulb when fadetime was 0
449
+ transition_time = limit(int(transition * 10), 1, 65535)
450
+ reference = int(time.time())
451
+ return self._queuedevicestatus(
452
+ sleepfader=(transition_time, reference)
453
+ )
454
+
455
+ return self._queuedevicestatus(onoff=OFF)
456
+
457
+ def set_temperature(
458
+ self,
459
+ kelvin: int = 2700,
460
+ mireds: int | None = None,
461
+ transition: int = 0,
462
+ delay: bool = True,
463
+ ) -> Light:
464
+ """Set the color temperature of the light."""
465
+ transition_time = limit(int(transition * 10), 0, 65535)
466
+ if mireds is None:
467
+ mireds = int(1000000 / kelvin)
468
+ mireds = limit(int(mireds), *self.temperature_range)
469
+ return self._queuedevicestatus(
470
+ colortemperature=(mireds, transition_time), queue=delay
471
+ )
472
+
473
+ def set_color(
474
+ self, colorxy: ColorXY, transition: int = 0, delay: bool = True
475
+ ) -> Light:
476
+ """Set the color of the light."""
477
+ transition_time = limit(int(transition * 10), 0, 65535)
478
+ colorxy = limit_to_gamut(colorxy, self.gamut)
479
+ colorx = limit(int(colorxy[0] * 65535), 0, 65535)
480
+ colory = limit(int(colorxy[1] * 65535), 0, 65535)
481
+ return self._queuedevicestatus(
482
+ colorcontrol=(colorx, colory, transition_time), queue=delay
483
+ )
484
+
485
+ def start_ramp(self, ramp_up: bool, rate: int) -> Light:
486
+ """Start ramping the brightness up or down."""
487
+ up_down = "1" if ramp_up else "0"
488
+ rate = limit(int(rate), 0, 255)
489
+ return self._queuedevicestatus(levelcontrol_move=(up_down, rate))
490
+
491
+ def stop_ramp(self) -> LinkedDevice:
492
+ """Start ramping the brightness up or down."""
493
+ return self._setdevicestatus(levelcontrol_stop="")
494
+
495
+
496
+ class Group(LinkedDevice):
497
+ """Representation of a Group of lights connected to the Bridge."""
498
+
499
+ _NAME_TAG = "GroupName"
500
+ _CAPABILITIES_TAGS = ("GroupCapabilityIDs", "CapabilityID")
501
+ _VALUES_TAGS = ("GroupCapabilityValues", "CapabilityValue")
502
+
503
+ def __init__(self, bridge: Bridge, info: et._Element) -> None:
504
+ """Create a Group device."""
505
+ super().__init__(bridge, info)
506
+ self.uniqueID: str = info.findtext("GroupID", "")
@@ -0,0 +1,92 @@
1
+ """Representation of a WeMo CoffeeMaker device."""
2
+ from __future__ import annotations
3
+
4
+ from enum import IntEnum
5
+ from typing import Any, TypedDict
6
+
7
+ from .api.attributes import AttributeDevice
8
+
9
+ _UNKNOWN = -1
10
+
11
+
12
+ # These enums were derived from the
13
+ # CoffeeMaker.deviceevent.GetAttributeList() service call
14
+ # Thus these names/values were not chosen randomly
15
+ # and the numbers have meaning.
16
+ class CoffeeMakerMode(IntEnum):
17
+ """Enum to map WeMo modes to human-readable strings."""
18
+
19
+ _UNKNOWN = _UNKNOWN
20
+ # Note: The UpperMixedCase (invalid) names are deprecated.
21
+ REFILL = 0 # reservoir empty and carafe not in place
22
+ Refill = 0 # pylint: disable=invalid-name
23
+ PLACE_CARAFE = 1 # reservoir has water but carafe not present
24
+ PlaceCarafe = 1 # pylint: disable=invalid-name
25
+ REFILL_WATER = 2 # carafe present but reservoir is empty
26
+ RefillWater = 2 # pylint: disable=invalid-name
27
+ READY = 3
28
+ Ready = 3 # pylint: disable=invalid-name
29
+ BREWING = 4
30
+ Brewing = 4 # pylint: disable=invalid-name
31
+ BREWED = 5
32
+ Brewed = 5 # pylint: disable=invalid-name
33
+ CLEANING_BREWING = 6
34
+ CleaningBrewing = 6 # pylint: disable=invalid-name
35
+ CLEANING_SOAKING = 7
36
+ CleaningSoaking = 7 # pylint: disable=invalid-name
37
+ BREW_FAILED_CARAFE_REMOVED = 8
38
+ BrewFailCarafeRemoved = 8 # pylint: disable=invalid-name
39
+
40
+ @classmethod
41
+ def _missing_(cls, value: Any) -> CoffeeMakerMode:
42
+ return cls._UNKNOWN
43
+
44
+
45
+ MODE_NAMES = {
46
+ CoffeeMakerMode.REFILL: "Refill",
47
+ CoffeeMakerMode.PLACE_CARAFE: "PlaceCarafe",
48
+ CoffeeMakerMode.REFILL_WATER: "RefillWater",
49
+ CoffeeMakerMode.READY: "Ready",
50
+ CoffeeMakerMode.BREWING: "Brewing",
51
+ CoffeeMakerMode.BREWED: "Brewed",
52
+ CoffeeMakerMode.CLEANING_BREWING: "CleaningBrewing",
53
+ CoffeeMakerMode.CLEANING_SOAKING: "CleaningSoaking",
54
+ CoffeeMakerMode.BREW_FAILED_CARAFE_REMOVED: "BrewFailCarafeRemoved",
55
+ }
56
+
57
+
58
+ class _Attributes(TypedDict, total=False):
59
+ Mode: int
60
+
61
+
62
+ class CoffeeMaker(AttributeDevice):
63
+ """Representation of a WeMo CoffeeMaker device."""
64
+
65
+ _state_property = "mode" # Required by AttributeDevice.
66
+ _attributes: _Attributes # Required by AttributeDevice.
67
+
68
+ @property
69
+ def mode(self) -> CoffeeMakerMode:
70
+ """Return the mode of the device."""
71
+ return CoffeeMakerMode(self._attributes.get("Mode", _UNKNOWN))
72
+
73
+ @property
74
+ def mode_string(self) -> str:
75
+ """Return the mode of the device as a string."""
76
+ return MODE_NAMES.get(self.mode, "Unknown")
77
+
78
+ def get_state(self, force_update: bool = False) -> int:
79
+ """Return 0 if off and 1 if on."""
80
+ # The base implementation using GetBinaryState doesn't work for
81
+ # CoffeeMaker (always returns 0), so use mode instead.
82
+ # Consider the Coffee Maker to be "on" if it's currently brewing.
83
+ return int(super().get_state(force_update) == CoffeeMakerMode.BREWING)
84
+
85
+ def set_state(self, state: int) -> None:
86
+ """Set the state of this device to on or off."""
87
+ # CoffeeMaker cannot be turned off remotely, so ignore the request if
88
+ # state is "falsey"
89
+ if state:
90
+ # Coffee Maker always responds with an error if SetBinaryState is
91
+ # called. Use SetAttributes to change the Mode to "Brewing"
92
+ self._set_attributes(("Mode", CoffeeMakerMode.BREWING))