pywemo 1.3.1__py3-none-any.whl → 2.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.
pywemo/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  r"""Lightweight Python module to discover and control WeMo devices.
2
+
2
3
  .. include:: README.md
3
4
  """
4
- # flake8: noqa F401
5
5
 
6
6
  from .discovery import (
7
7
  device_from_description,
pywemo/color.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Various utilities for handling colors."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from typing import Tuple
@@ -6,19 +7,19 @@ from typing import Tuple
6
7
  # Define usable ranges as bulbs either ignore or behave unexpectedly
7
8
  # when it is sent a value is outside of the range.
8
9
  TemperatureRange = Tuple[int, int]
9
- TEMPERATURE_PROFILES: dict[str, TemperatureRange] = dict(
10
- (model, temp)
10
+ TEMPERATURE_PROFILES: dict[str, TemperatureRange] = {
11
+ model: temp
11
12
  for models, temp in (
12
13
  # Lightify RGBW, 1900-6500K
13
14
  (["LIGHTIFY A19 RGBW"], (151, 555)),
14
15
  )
15
16
  for model in models
16
- )
17
+ }
17
18
 
18
19
  ColorXY = Tuple[float, float]
19
20
  ColorGamut = Tuple[ColorXY, ColorXY, ColorXY]
20
- COLOR_PROFILES: dict[str, ColorGamut] = dict(
21
- (model, gamut)
21
+ COLOR_PROFILES: dict[str, ColorGamut] = {
22
+ model: gamut
22
23
  for models, gamut in (
23
24
  # Lightify RGBW, 1900-6500K
24
25
  # https://flow-morewithless.blogspot.com/2015/01/osram-lightify-color-gamut-and-spectrum.html
@@ -28,7 +29,7 @@ COLOR_PROFILES: dict[str, ColorGamut] = dict(
28
29
  ),
29
30
  )
30
31
  for model in models
31
- )
32
+ }
32
33
 
33
34
 
34
35
  def get_profiles(model: str) -> tuple[TemperatureRange, ColorGamut]:
@@ -42,9 +43,9 @@ def get_profiles(model: str) -> tuple[TemperatureRange, ColorGamut]:
42
43
  def is_same_side(p1: ColorXY, p2: ColorXY, a: ColorXY, b: ColorXY) -> bool:
43
44
  """Test if points p1 and p2 lie on the same side of line a-b."""
44
45
  # pylint: disable=invalid-name
45
- vector_ab = [y - x for x, y in zip(a, b)]
46
- vector_ap1 = [y - x for x, y in zip(a, p1)]
47
- vector_ap2 = [y - x for x, y in zip(a, p2)]
46
+ vector_ab = [y - x for x, y in zip(a, b, strict=True)]
47
+ vector_ap1 = [y - x for x, y in zip(a, p1, strict=True)]
48
+ vector_ap2 = [y - x for x, y in zip(a, p2, strict=True)]
48
49
  cross_vab_ap1 = vector_ab[0] * vector_ap1[1] - vector_ab[1] * vector_ap1[0]
49
50
  cross_vab_ap2 = vector_ab[0] * vector_ap2[1] - vector_ab[1] * vector_ap2[0]
50
51
  return (cross_vab_ap1 * cross_vab_ap2) >= 0
@@ -53,10 +54,10 @@ def is_same_side(p1: ColorXY, p2: ColorXY, a: ColorXY, b: ColorXY) -> bool:
53
54
  def closest_point(p: ColorXY, a: ColorXY, b: ColorXY) -> ColorXY:
54
55
  """Test if points p1 and p2 lie on the same side of line a-b."""
55
56
  # pylint: disable=invalid-name
56
- vector_ab = [y - x for x, y in zip(a, b)]
57
- vector_ap = [y - x for x, y in zip(a, p)]
58
- dot_ap_ab = sum(x * y for x, y in zip(vector_ap, vector_ab))
59
- dot_ab_ab = sum(x * y for x, y in zip(vector_ab, vector_ab))
57
+ vector_ab = [y - x for x, y in zip(a, b, strict=True)]
58
+ vector_ap = [y - x for x, y in zip(a, p, strict=True)]
59
+ dot_ap_ab = sum(x * y for x, y in zip(vector_ap, vector_ab, strict=True))
60
+ dot_ab_ab = sum(x * y for x, y in zip(vector_ab, vector_ab, strict=True))
60
61
  t = max(0.0, min(dot_ap_ab / dot_ab_ab, 1.0))
61
62
  return a[0] + vector_ab[0] * t, a[1] + vector_ab[1] * t
62
63
 
pywemo/discovery.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Module to discover WeMo devices."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
pywemo/exceptions.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Exceptions raised by pywemo."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from lxml import etree as et
@@ -1,4 +1,5 @@
1
1
  """Base WeMo Device class."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import base64
@@ -6,7 +7,8 @@ import logging
6
7
  import subprocess
7
8
  import threading
8
9
  import time
9
- from typing import Any, Sequence
10
+ from collections.abc import Sequence
11
+ from typing import Any
10
12
 
11
13
  import requests
12
14
 
@@ -17,6 +19,7 @@ from ..exceptions import (
17
19
  ResetException,
18
20
  SetupException,
19
21
  ShortPassword,
22
+ SOAPFault,
20
23
  UnknownService,
21
24
  )
22
25
  from ..util import MetaInfo
@@ -133,8 +136,7 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
133
136
  ]
134
137
 
135
138
  def _reconnect_with_device_by_discovery(self) -> None:
136
- """
137
- Scan network to find the device again.
139
+ """Scan network to find the device again.
138
140
 
139
141
  Wemos tend to change their port number from time to time.
140
142
  Whenever requests throws an error, we will try to find the device again
@@ -260,7 +262,7 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
260
262
  Set to True to clear wifi information ("Change Wi-Fi" in the Wemo
261
263
  app), which does not clear the rules, name, etc.
262
264
 
263
- Notes
265
+ Notes:
264
266
  -----
265
267
  Setting both to true is equivalent to a "Factory Restore" from the app.
266
268
 
@@ -272,6 +274,7 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
272
274
  ReSetup action below were consistent. These could potentially change
273
275
  in a future firmware revision or may be different for other untested
274
276
  devices.
277
+
275
278
  """
276
279
  try:
277
280
  action = self.basicevent.ReSetup
@@ -312,23 +315,68 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
312
315
 
313
316
  @staticmethod
314
317
  def encrypt_aes128(
315
- password: str, wemo_metadata: str, is_rtos: bool
318
+ password: str, wemo_metadata: str, method: int, add_lengths: bool
316
319
  ) -> str:
317
320
  """Encrypt a password using OpenSSL.
318
321
 
319
322
  Function borrows heavily from Vadim Kantorov's "wemosetup" script:
320
323
  https://github.com/vadimkantorov/wemosetup
321
324
  """
325
+ # pylint: disable=too-many-branches, too-many-locals
322
326
  if not password:
323
327
  raise SetupException("password required for AES")
324
328
 
325
329
  # Wemo uses some meta information for salt and iv
326
330
  meta_info = MetaInfo.from_meta_info(wemo_metadata)
327
- keydata = (
328
- meta_info.mac[:6] + meta_info.serial_number + meta_info.mac[6:12]
331
+ mac = meta_info.mac
332
+ serial = meta_info.serial_number
333
+
334
+ LOG.debug(
335
+ "Using encryption method=%s and add_lengths=%s",
336
+ method,
337
+ add_lengths,
329
338
  )
330
- if is_rtos:
339
+ if method == 1:
340
+ # the original method
341
+ keydata = mac[:6] + serial + mac[6:12]
342
+ elif method == 2:
343
+ # rtos=1 (or maybe new_algo=1)
344
+ keydata = mac[:6] + serial + mac[6:12]
331
345
  keydata += "b3{8t;80dIN{ra83eC1s?M70?683@2Yf"
346
+ elif method == 3:
347
+ # binaryOption = 1
348
+ characters = "".join(
349
+ [
350
+ "Onboard",
351
+ "$",
352
+ "Application",
353
+ "@",
354
+ "Device",
355
+ "&",
356
+ "Information",
357
+ "#",
358
+ "Wemo",
359
+ ]
360
+ )
361
+ mixed = ""
362
+ for i, char in enumerate(characters):
363
+ if i % 2:
364
+ mixed = mixed + char
365
+ else:
366
+ mixed = char + mixed
367
+ extra = base64.b64encode(mixed.encode()).decode()[:32]
368
+
369
+ # the above transformation results in the following strings, but
370
+ # the calculation above is left for posterity
371
+ # --> characters = "Onboard$Application@Device&Information#Wemo"
372
+ # --> mixed = 'oe#otmon&cvDniaipAdabOnor$plcto@eieIfrainWm'
373
+ # --> extra = 'b2Ujb3Rtb24mY3ZEbmlhaXBBZGFiT25v'
374
+
375
+ keydata = (
376
+ mac[:3] + mac[9:12] + serial + extra + mac[6:9] + mac[3:6]
377
+ )
378
+ else:
379
+ raise SetupException(f"{method=} must be 1, 2, or 3")
332
380
 
333
381
  salt, initialization_vector = keydata[:8], keydata[:16]
334
382
  if len(salt) != 8 or len(initialization_vector) != 16:
@@ -383,12 +431,22 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
383
431
  f"{len_original} (and {len_encrypted} after encryption)."
384
432
  )
385
433
 
386
- if not is_rtos:
434
+ if add_lengths:
387
435
  encrypted_password += f"{len_encrypted:#04x}"[2:]
388
436
  encrypted_password += f"{len_original:#04x}"[2:]
389
437
  return encrypted_password
390
438
 
391
- def setup(self, *args: Any, **kwargs: Any) -> tuple[str, str]:
439
+ def setup(
440
+ self,
441
+ ssid: str,
442
+ password: str,
443
+ *,
444
+ timeout: float = 20.0,
445
+ connection_attempts: int = 1,
446
+ status_delay: float = 1.0,
447
+ _encrypt_method: int | None = None,
448
+ _add_password_lengths: bool | None = None,
449
+ ) -> tuple[str, str]:
392
450
  """Connect Wemo to wifi network.
393
451
 
394
452
  This function should be used and will capture several potential
@@ -414,14 +472,32 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
414
472
  status of the device. Generally should prefer this to be as short
415
473
  as possible, but not too quick to overload the device with
416
474
  requests. It must be less than or equal to half of the `timeout`.
417
-
418
- Notes
475
+ _encrypt_method (int | None, optional):
476
+ Override the pywemo detection for which of the 3 encryption methods
477
+ to use. If set, must be 1, 2, or 3. The default, None, will use
478
+ the automatically detected method. This should generally be left
479
+ as the default value.
480
+ _add_password_lengths (bool | None, optional):
481
+ Override the pywemo detection for whether to add the password
482
+ lengths to the encrypted password. Similar to _encrypt_method,
483
+ this should generally be left as the default None value.
484
+
485
+ Notes:
419
486
  -----
420
487
  The timeout applies to each connection attempt, so the total wait time
421
488
  will be approximately `timeout * connection_attempts`.
489
+
422
490
  """
423
491
  try:
424
- return self._setup(*args, **kwargs)
492
+ return self._setup(
493
+ ssid=ssid,
494
+ password=password,
495
+ timeout=timeout,
496
+ connection_attempts=connection_attempts,
497
+ status_delay=status_delay,
498
+ _encrypt_method=_encrypt_method,
499
+ _add_password_lengths=_add_password_lengths,
500
+ )
425
501
  except (UnknownService, AttributeError, KeyError) as exc:
426
502
  # Exception | Reason to catch it
427
503
  # --------------------------------------------------------------
@@ -449,14 +525,17 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
449
525
  ) from exc
450
526
 
451
527
  def _setup( # noqa: C901
452
- # pylint: disable=too-many-arguments,too-many-branches,too-many-locals
453
- # pylint: disable=too-many-statements
528
+ # pylint: disable=too-many-branches, too-many-locals
529
+ # pylint: disable=too-many-statements, too-many-positional-arguments
454
530
  self,
455
531
  ssid: str,
456
532
  password: str,
457
- timeout: float = 20.0,
458
- connection_attempts: int = 1,
459
- status_delay: float = 1.0,
533
+ *,
534
+ timeout: float,
535
+ connection_attempts: int,
536
+ status_delay: float,
537
+ _encrypt_method: int | None,
538
+ _add_password_lengths: bool | None,
460
539
  ) -> tuple[str, str]:
461
540
  """Connect Wemo to wifi network.
462
541
 
@@ -467,6 +546,14 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
467
546
  timeout = max(timeout, 20.0)
468
547
  status_delay = min(status_delay, timeout / 2.0)
469
548
  connection_attempts = int(max(1, connection_attempts))
549
+ LOG.debug(
550
+ "Trying to connect to AP %s with timeout=%s, "
551
+ "connection_attempts=%s, and status_delay=%s",
552
+ ssid,
553
+ timeout,
554
+ connection_attempts,
555
+ status_delay,
556
+ )
470
557
 
471
558
  # find all access points that the device can see, and select the one
472
559
  # matching the desired SSID
@@ -491,17 +578,24 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
491
578
  # get some information about the access point
492
579
  columns = selected_ap.split("|")
493
580
  channel = columns[1].strip()
494
- auth_mode, encryption_method = columns[-1].strip().split("/")
581
+ auth_string = columns[-1].strip()
582
+ if auth_string == "Unknown":
583
+ raise SetupException(
584
+ f"Wemo reports AP {ssid} with 'Unknown' authorization mode. "
585
+ "AP may be using an unsupported authorization mode (ex WPA3)"
586
+ )
587
+
588
+ auth_mode, encryption_method = auth_string.split("/")
495
589
  LOG.debug("AP channel: %s", channel)
496
590
  LOG.debug("AP authorization mode(s): %s", auth_mode)
497
591
  LOG.debug("AP encryption method: %s", encryption_method)
498
592
 
499
593
  # check if the encryption type is supported by this script
500
- supported_encryptions = {"NONE", "AES"}
594
+ supported_encryptions = {"NONE", "AES", "TKIPAES"}
501
595
  if encryption_method not in supported_encryptions:
502
596
  raise SetupException(
503
597
  f"Encryption {encryption_method} not currently supported. "
504
- f'Supported encryptions are: {",".join(supported_encryptions)}'
598
+ f"Supported encryptions are: {','.join(supported_encryptions)}"
505
599
  )
506
600
 
507
601
  # try to connect the device to the selected network
@@ -512,9 +606,42 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
512
606
  else:
513
607
  # get the meta information of the device and encrypt the password
514
608
  meta_info = self.get_service("metainfo").GetMetaInfo()["MetaInfo"]
515
- is_rtos = self._config_any.get("rtos", "0") == "1"
609
+
610
+ if _encrypt_method is None:
611
+ # investigation of the android APK indicates this logic:
612
+ # --> if self._config_any.get('binaryOption', "0") == "1":
613
+ # --> method = 3
614
+ # --> elif self._config_any.get('new_algo', "0") == "1":
615
+ # --> method = 2
616
+ # --> else:
617
+ # --> method = 1
618
+ # --> add_lengths = True # for all 3 methods
619
+
620
+ # however, this works correctly more often than the logic above
621
+ is_rtos = self._config_any.get("rtos", "0") == "1"
622
+ if is_rtos:
623
+ method = 2
624
+ else:
625
+ method = 1
626
+ LOG.debug(
627
+ "Automatically detected encryption method=%s", method
628
+ )
629
+ else:
630
+ method = _encrypt_method
631
+
632
+ if _add_password_lengths is None:
633
+ # by default, add the lengths for methods 1 and 3, but not 2
634
+ add_lengths = method in (1, 3)
635
+ LOG.debug(
636
+ "Automatically detected encryption add password lengths="
637
+ "%s",
638
+ add_lengths,
639
+ )
640
+ else:
641
+ add_lengths = _add_password_lengths
642
+
516
643
  encrypted_password = self.encrypt_aes128(
517
- password, meta_info, is_rtos
644
+ password, meta_info, method, add_lengths
518
645
  )
519
646
 
520
647
  # optionally make multiple connection attempts
@@ -585,7 +712,7 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
585
712
 
586
713
  try:
587
714
  result = wifisetup.CloseSetup()
588
- except AttributeError:
715
+ except (AttributeError, SOAPFault):
589
716
  # if CloseSetup doesn't exist, it may still work
590
717
  result = {"status": "CloseSetup action not available"}
591
718
 
@@ -609,7 +736,7 @@ class Device(DeviceDescription, RequiredServicesMixin, WeMoServiceTypesMixin):
609
736
  if status == "1" and close_status == "success":
610
737
  try:
611
738
  self.basicevent.SetSetupDoneStatus()
612
- except AttributeError:
739
+ except (AttributeError, SOAPFault):
613
740
  LOG.debug(
614
741
  "SetSetupDoneStatus not available (some devices do not "
615
742
  "have this method)"
@@ -1,4 +1,5 @@
1
1
  """Attribute device helpers."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Light-weight mapping between sqlite3 and python data structures."""
2
+
2
3
  import logging
3
4
  import sqlite3
4
5
  from typing import Any, Callable, Dict
@@ -36,7 +37,7 @@ class DatabaseRow:
36
37
  values = []
37
38
  for name in self.FIELDS.keys():
38
39
  if hasattr(self, name):
39
- values.append(f"{name}={repr(getattr(self, name))}")
40
+ values.append(f"{name}={getattr(self, name)!r}")
40
41
  class_name = self.__class__.__name__
41
42
  values_str = ", ".join(values)
42
43
  return f"{class_name}({values_str})"
@@ -158,6 +159,7 @@ class SQLType:
158
159
  the database cursor and convert it to a Python type.
159
160
  sql_type: The sqlite field type.
160
161
  not_null: Whether or not the sqlite field can be null.
162
+
161
163
  """
162
164
  self.type_constructor = type_constructor
163
165
  self._sql_type = (
@@ -7,11 +7,13 @@ configured for the device, it will turn on/off/toggle other Wemo devices on the
7
7
  network. The methods in this mixin allow editing of the devices that are
8
8
  controlled by a long press.
9
9
  """
10
+
10
11
  from __future__ import annotations
11
12
 
12
13
  import logging
14
+ from collections.abc import Iterable
13
15
  from enum import Enum
14
- from typing import Iterable, no_type_check
16
+ from typing import no_type_check
15
17
 
16
18
  from .rules_db import RuleDevicesRow, RulesDb, RulesRow, rules_db_from_device
17
19
  from .service import RequiredService, RequiredServicesMixin
@@ -8,8 +8,9 @@ import os
8
8
  import sqlite3
9
9
  import tempfile
10
10
  import zipfile
11
+ from collections.abc import Mapping
11
12
  from types import MappingProxyType
12
- from typing import FrozenSet, List, Mapping, Optional, Tuple
13
+ from typing import FrozenSet, List, Tuple
13
14
 
14
15
  from pywemo.exceptions import HTTPNotOkException, RulesDbQueryError
15
16
 
@@ -273,8 +274,8 @@ class RulesDb:
273
274
  def rules_for_device(
274
275
  self,
275
276
  *,
276
- device_udn: Optional[str] = None,
277
- rule_type: Optional[str] = None,
277
+ device_udn: str | None = None,
278
+ rule_type: str | None = None,
278
279
  ) -> List[Tuple[RulesRow, RuleDevicesRow]]:
279
280
  """Fetch the current rules for a particular device."""
280
281
  if device_udn is None:
@@ -305,7 +306,7 @@ class RulesDb:
305
306
  rule: RulesRow,
306
307
  device_id: str,
307
308
  *,
308
- device_index: Optional[int] = None,
309
+ device_index: int | None = None,
309
310
  ):
310
311
  """Add a new target DeviceID to the rule."""
311
312
  if device_index is None:
@@ -1,10 +1,12 @@
1
1
  """Representation of Services and Actions for WeMo devices."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
5
6
  from collections import defaultdict
7
+ from collections.abc import Iterable
6
8
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Any, Iterable
9
+ from typing import TYPE_CHECKING, Any
8
10
  from urllib.parse import urljoin, urlparse
9
11
 
10
12
  import urllib3
@@ -37,7 +39,7 @@ REQUEST_TEMPLATE = """
37
39
  </u:{action}>
38
40
  </s:Body>
39
41
  </s:Envelope>
40
- """ # noqa: E501
42
+ """
41
43
 
42
44
 
43
45
  class Session:
@@ -95,7 +97,7 @@ class Session:
95
97
  retries: int | urllib3.Retry | None = None,
96
98
  timeout: float | None = None,
97
99
  **kwargs: Any,
98
- ) -> urllib3.HTTPResponse:
100
+ ) -> urllib3.BaseHTTPResponse:
99
101
  """Send request and gather response.
100
102
 
101
103
  A non-200 response code will result in a HTTPException
@@ -111,6 +113,7 @@ class Session:
111
113
  Raises:
112
114
  HTTPNotOkException: when the response code is not 200.
113
115
  HTTPException: for any urllib3 exception.
116
+
114
117
  """
115
118
  if retries is None:
116
119
  retries = self.retries
@@ -122,7 +125,7 @@ class Session:
122
125
  # http connection is also closed. This avoids tying up TCP sessions
123
126
  # on the device.
124
127
  with urllib3.PoolManager(retries=retries, timeout=timeout) as pool:
125
- response: urllib3.HTTPResponse
128
+ response: urllib3.BaseHTTPResponse
126
129
  try:
127
130
  response = pool.request(method=method, url=url, **kwargs)
128
131
  if response.status != 200:
@@ -135,11 +138,11 @@ class Session:
135
138
  response.content = response.data # type: ignore
136
139
  return response
137
140
 
138
- def get(self, url: str, **kwargs: Any) -> urllib3.HTTPResponse:
141
+ def get(self, url: str, **kwargs: Any) -> urllib3.BaseHTTPResponse:
139
142
  """HTTP GET request."""
140
143
  return self.request("GET", url, **kwargs)
141
144
 
142
- def post(self, url: str, **kwargs: Any) -> urllib3.HTTPResponse:
145
+ def post(self, url: str, **kwargs: Any) -> urllib3.BaseHTTPResponse:
143
146
  """HTTP POST request."""
144
147
  return self.request("POST", url, **kwargs)
145
148
 
@@ -278,7 +281,7 @@ class Action:
278
281
  class Service(WeMoAllActionsMixin):
279
282
  """Representation of a service for a WeMo device."""
280
283
 
281
- def __init__(self, device: "Device", service: ServiceProperties) -> None:
284
+ def __init__(self, device: Device, service: ServiceProperties) -> None:
282
285
  """Create an instance of a Service."""
283
286
  self.device = device
284
287
  self._config = service
@@ -10,7 +10,9 @@ To regenerate this file, run:
10
10
 
11
11
  from typing import Callable
12
12
 
13
- UPnPMethod = Callable[..., dict[str, str]]
13
+ from typing_extensions import TypeAlias
14
+
15
+ UPnPMethod: TypeAlias = Callable[..., dict[str, str]]
14
16
 
15
17
  class Service_WiFiSetup:
16
18
  CloseSetup: UPnPMethod
@@ -237,5 +239,4 @@ class WeMoAllActionsMixin(
237
239
  Service_rules,
238
240
  Service_smartsetup,
239
241
  Service_timesync,
240
- ):
241
- pass
242
+ ): ...
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
- # flake8: noqa
4
- # isort: skip_file
5
3
  # mypy: ignore-errors
6
4
  # pylint: skip-file
7
5
 
8
6
  #
9
- # Generated by generateDS.py version 2.43.2.
7
+ # Generated by generateDS.py version 2.44.3.
10
8
  # Python [sys.version]
11
9
  #
12
10
  # Command line options:
@@ -191,7 +189,9 @@ except ModulenotfoundExp_ as exp:
191
189
 
192
190
  class GeneratedsSuper(GeneratedsSuperSuper):
193
191
  __hash__ = object.__hash__
194
- tzoff_pattern = re_.compile(r"(\+|-)((0\d|1[0-3]):[0-5]\d|14:00)$")
192
+ tzoff_pattern = re_.compile(
193
+ "(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)$"
194
+ )
195
195
 
196
196
  class _FixedOffsetTZ(datetime_.tzinfo):
197
197
  def __init__(self, offset, name):
@@ -436,8 +436,7 @@ except ModulenotfoundExp_ as exp:
436
436
  0,
437
437
  ):
438
438
  raise_parse_error(
439
- node,
440
- "Requires boolean value " "(one of True, 1, False, 0)",
439
+ node, "Requires boolean value (one of True, 1, False, 0)"
441
440
  )
442
441
  return input_data
443
442
 
@@ -746,7 +745,7 @@ except ModulenotfoundExp_ as exp:
746
745
  path = "/".join(path_list)
747
746
  return path
748
747
 
749
- Tag_strip_pattern_ = re_.compile(r"\{.*\}")
748
+ Tag_strip_pattern_ = re_.compile(r"{.*}")
750
749
 
751
750
  def get_path_list_(self, node, path_list):
752
751
  if node is None:
@@ -1235,7 +1234,7 @@ class root(GeneratedsSuper):
1235
1234
  URLBase=None,
1236
1235
  device=None,
1237
1236
  gds_collector_=None,
1238
- **kwargs_
1237
+ **kwargs_,
1239
1238
  ):
1240
1239
  self.gds_collector_ = gds_collector_
1241
1240
  self.gds_elementtree_node_ = None
@@ -1753,7 +1752,7 @@ class DeviceType(GeneratedsSuper):
1753
1752
  presentationURL=None,
1754
1753
  anytypeobjs_=None,
1755
1754
  gds_collector_=None,
1756
- **kwargs_
1755
+ **kwargs_,
1757
1756
  ):
1758
1757
  self.gds_collector_ = gds_collector_
1759
1758
  self.gds_elementtree_node_ = None
@@ -3011,7 +3010,7 @@ class iconType(GeneratedsSuper):
3011
3010
  depth=None,
3012
3011
  url=None,
3013
3012
  gds_collector_=None,
3014
- **kwargs_
3013
+ **kwargs_,
3015
3014
  ):
3016
3015
  self.gds_collector_ = gds_collector_
3017
3016
  self.gds_elementtree_node_ = None
@@ -3322,7 +3321,7 @@ class serviceType(GeneratedsSuper):
3322
3321
  controlURL=None,
3323
3322
  eventSubURL=None,
3324
3323
  gds_collector_=None,
3325
- **kwargs_
3324
+ **kwargs_,
3326
3325
  ):
3327
3326
  self.gds_collector_ = gds_collector_
3328
3327
  self.gds_elementtree_node_ = None
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
- # flake8: noqa
4
- # isort: skip_file
5
3
  # mypy: ignore-errors
6
4
  # pylint: skip-file
7
5
 
8
6
  #
9
- # Generated by generateDS.py version 2.43.2.
7
+ # Generated by generateDS.py version 2.44.3.
10
8
  # Python [sys.version]
11
9
  #
12
10
  # Command line options:
@@ -191,7 +189,9 @@ except ModulenotfoundExp_ as exp:
191
189
 
192
190
  class GeneratedsSuper(GeneratedsSuperSuper):
193
191
  __hash__ = object.__hash__
194
- tzoff_pattern = re_.compile(r"(\+|-)((0\d|1[0-3]):[0-5]\d|14:00)$")
192
+ tzoff_pattern = re_.compile(
193
+ "(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)$"
194
+ )
195
195
 
196
196
  class _FixedOffsetTZ(datetime_.tzinfo):
197
197
  def __init__(self, offset, name):
@@ -436,8 +436,7 @@ except ModulenotfoundExp_ as exp:
436
436
  0,
437
437
  ):
438
438
  raise_parse_error(
439
- node,
440
- "Requires boolean value " "(one of True, 1, False, 0)",
439
+ node, "Requires boolean value (one of True, 1, False, 0)"
441
440
  )
442
441
  return input_data
443
442
 
@@ -746,7 +745,7 @@ except ModulenotfoundExp_ as exp:
746
745
  path = "/".join(path_list)
747
746
  return path
748
747
 
749
- Tag_strip_pattern_ = re_.compile(r"\{.*\}")
748
+ Tag_strip_pattern_ = re_.compile(r"{.*}")
750
749
 
751
750
  def get_path_list_(self, node, path_list):
752
751
  if node is None:
@@ -1235,7 +1234,7 @@ class scpd(GeneratedsSuper):
1235
1234
  actionList=None,
1236
1235
  serviceStateTable=None,
1237
1236
  gds_collector_=None,
1238
- **kwargs_
1237
+ **kwargs_,
1239
1238
  ):
1240
1239
  self.gds_collector_ = gds_collector_
1241
1240
  self.gds_elementtree_node_ = None
@@ -2223,7 +2222,7 @@ class ArgumentType(GeneratedsSuper):
2223
2222
  relatedStateVariable=None,
2224
2223
  retval=None,
2225
2224
  gds_collector_=None,
2226
- **kwargs_
2225
+ **kwargs_,
2227
2226
  ):
2228
2227
  self.gds_collector_ = gds_collector_
2229
2228
  self.gds_elementtree_node_ = None
@@ -2696,7 +2695,7 @@ class StateVariableType(GeneratedsSuper):
2696
2695
  allowedValueList=None,
2697
2696
  allowedValueRange=None,
2698
2697
  gds_collector_=None,
2699
- **kwargs_
2698
+ **kwargs_,
2700
2699
  ):
2701
2700
  self.gds_collector_ = gds_collector_
2702
2701
  self.gds_elementtree_node_ = None
@@ -3218,7 +3217,7 @@ class AllowedValueRangeType(GeneratedsSuper):
3218
3217
  maximum=None,
3219
3218
  step=None,
3220
3219
  gds_collector_=None,
3221
- **kwargs_
3220
+ **kwargs_,
3222
3221
  ):
3223
3222
  self.gds_collector_ = gds_collector_
3224
3223
  self.gds_elementtree_node_ = None
@@ -6,6 +6,7 @@ provided for optional fields. Clients of this module can expect that all
6
6
  fields of the dataclass instances are fully populated and valid. Any parsing
7
7
  or validation issues will result in InvalidSchemaError being raised.
8
8
  """
9
+
9
10
  from __future__ import annotations
10
11
 
11
12
  import logging
@@ -1,11 +1,13 @@
1
1
  """Representation of a WeMo Bridge (Link) device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import io
5
6
  import time
6
7
  import warnings
8
+ from collections.abc import Iterable
7
9
  from html import escape
8
- from typing import Any, Iterable, TypedDict
10
+ from typing import Any, TypedDict
9
11
 
10
12
  from lxml import etree as et
11
13
 
@@ -250,7 +252,11 @@ class LinkedDevice: # pylint: disable=too-many-instance-attributes
250
252
  )
251
253
  if current_state := get_first_text(self._VALUES_TAGS):
252
254
  self._update_values(
253
- zip(self.capabilities, current_state.split(","))
255
+ zip(
256
+ self.capabilities,
257
+ current_state.split(","),
258
+ strict=True,
259
+ )
254
260
  )
255
261
 
256
262
  def _update_values(self, values: Iterable[tuple[str, str]]) -> None:
@@ -268,7 +274,7 @@ class LinkedDevice: # pylint: disable=too-many-instance-attributes
268
274
  )
269
275
  except ValueError as err:
270
276
  raise ValueError(
271
- f"Invalid value for {capability}: {repr(value)}"
277
+ f"Invalid value for {capability}: {value!r}"
272
278
  ) from err
273
279
 
274
280
  # unreachable devices have empty strings for all capability values
@@ -294,7 +300,7 @@ class LinkedDevice: # pylint: disable=too-many-instance-attributes
294
300
  if (color_control := status.get("colorcontrol")) is not None:
295
301
  if len(color_control) < 2:
296
302
  raise ValueError(
297
- f"Too few values for colorcontrol: {repr(color_control)}"
303
+ f"Too few values for colorcontrol: {color_control!r}"
298
304
  )
299
305
  color_x, color_y = float(color_control[0]), float(color_control[1])
300
306
  color_x, color_y = color_x / 65535.0, color_y / 65535.0
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo CoffeeMaker device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from enum import IntEnum
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo CrockPot device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Dimmer device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from .api.long_press import LongPressMixin
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Humidifier device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from enum import IntEnum
@@ -181,6 +182,7 @@ class Humidifier(AttributeDevice):
181
182
 
182
183
  Args:
183
184
  state: An int index of the FanMode IntEnum.
185
+
184
186
  """
185
187
  self.set_fan_mode(FanMode(state))
186
188
 
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Insight device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Motion device."""
2
+
2
3
  from .api.long_press import LongPressMixin
3
4
  from .switch import Switch
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Maker device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from typing import TypedDict
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Motion device."""
2
+
2
3
  from . import Device
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo OutdoorPlug device."""
2
+
2
3
  from .switch import Switch
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Representation of a WeMo Switch device."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from . import Device
pywemo/ssdp.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Module that implements SSDP protocol."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import logging
@@ -151,8 +152,7 @@ def scan( # pylint: disable=too-many-branches,too-many-locals
151
152
  max_entries: int | None = None,
152
153
  match_udn: str | None = None,
153
154
  ) -> list[UPNPEntry]:
154
- """
155
- Send a message over the network to discover upnp devices.
155
+ """Send a message over the network to discover upnp devices.
156
156
 
157
157
  Inspired by Crimsdings ChromeCast code
158
158
  https://github.com/crimsdings/ [ChromeCast repository since removed]
@@ -204,9 +204,7 @@ def scan( # pylint: disable=too-many-branches,too-many-locals
204
204
 
205
205
  # Search for devices
206
206
  if entry not in entries:
207
- if match_udn is None:
208
- entries.append(entry)
209
- elif match_udn == entry.udn:
207
+ if match_udn is None or match_udn == entry.udn:
210
208
  entries.append(entry)
211
209
 
212
210
  # Return if we've found the max number of devices
@@ -244,6 +242,7 @@ class DiscoveryResponder:
244
242
 
245
243
  Args:
246
244
  callback_port: The port for the SubscriptionRegistry HTTP server.
245
+
247
246
  """
248
247
  self.callback_port = callback_port
249
248
  self._thread: threading.Thread | None = None
pywemo/subscribe.py CHANGED
@@ -4,22 +4,25 @@ Example usage:
4
4
 
5
5
  ```python
6
6
  import pywemo
7
+
7
8
  # The SubscriptionRegistry maintains push subscriptions to each endpoint
8
9
  # of a device.
9
10
  registry = pywemo.SubscriptionRegistry()
10
11
  registry.start()
11
12
 
12
- device = ... # See example of discovering devices in the pywemo module.
13
+ device = ... # See example of discovering devices in the pywemo module.
13
14
 
14
15
  # Start subscribing to push notifications of state changes.
15
16
  registry.register(device)
16
17
 
18
+
17
19
  def push_notification(device, event, params):
18
20
  '''Notify device of state change and get new device state.'''
19
21
  processed_update = device.subscription_update(event, params)
20
22
  state = device.get_state(force_update=not processed_update)
21
23
  print(f"Device state: {state}")
22
24
 
25
+
23
26
  # Register a callback to receive state push notifications.
24
27
  registry.on(device, None, push_notification)
25
28
 
@@ -31,6 +34,7 @@ registry.unregister(device)
31
34
  registry.stop()
32
35
  ```
33
36
  """
37
+
34
38
  from __future__ import annotations
35
39
 
36
40
  import collections
@@ -197,6 +201,7 @@ class Subscription:
197
201
 
198
202
  Raises:
199
203
  requests.RequestException on error.
204
+
200
205
  """
201
206
  try:
202
207
  response = self._subscribe()
@@ -281,6 +286,7 @@ class Subscription:
281
286
 
282
287
  Returns:
283
288
  The duration of the subscription in seconds.
289
+
284
290
  """
285
291
  self.subscription_id = headers.get("SID", self.subscription_id)
286
292
  if timeout_header := headers.get("TIMEOUT", None):
@@ -336,7 +342,7 @@ def _start_server(port: int | None) -> HTTPServer:
336
342
  start_port = 8989
337
343
  ports_to_check = 128
338
344
 
339
- for offset in range(0, ports_to_check):
345
+ for offset in range(ports_to_check):
340
346
  port = start_port + offset
341
347
  try:
342
348
  return HTTPServer(("", port), RequestHandler)
@@ -462,9 +468,7 @@ class RequestHandler(BaseHTTPRequestHandler):
462
468
  if self.path.endswith("/upnp/control/basicevent1"):
463
469
  sender_ip, _ = self.client_address
464
470
  outer = self.server.outer
465
- for (
466
- device
467
- ) in outer._subscriptions: # pylint: disable=protected-access
471
+ for device in outer._subscriptions: # pylint: disable=protected-access
468
472
  if device.host != sender_ip:
469
473
  continue
470
474
  doc = self._get_xml_from_http_body()
pywemo/util.py CHANGED
@@ -1,17 +1,17 @@
1
1
  """Miscellaneous utility functions."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import os
5
6
  import socket
6
7
  from dataclasses import dataclass
7
- from datetime import datetime, timedelta
8
+ from datetime import datetime, timedelta, timezone
8
9
 
9
10
  import ifaddr
10
11
 
11
12
 
12
13
  def interface_addresses() -> list[str]:
13
- """
14
- Return local address for broadcast/multicast.
14
+ """Return local address for broadcast/multicast.
15
15
 
16
16
  Return local address of any network associated with a local interface
17
17
  that has broadcast (and probably multicast) capability.
@@ -132,7 +132,7 @@ class ExtMetaInfo: # pylint: disable=too-many-instance-attributes
132
132
  last_auth_value=int(values[3]),
133
133
  uptime=timedelta(hours=hours, minutes=minutes, seconds=seconds),
134
134
  firmware_update_state=int(values[5]),
135
- utc_time=datetime.utcfromtimestamp(int(values[6])),
135
+ utc_time=datetime.fromtimestamp(int(values[6]), timezone.utc),
136
136
  home_id=values[7],
137
137
  remote_access_enabled=bool(int(values[8])),
138
138
  model_name=values[9],
@@ -1,22 +1,25 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pywemo
3
- Version: 1.3.1
3
+ Version: 2.0.0
4
4
  Summary: Lightweight Python module to discover and control WeMo devices
5
- Home-page: https://github.com/pywemo/pywemo
6
5
  License: MIT
6
+ License-File: LICENSE
7
7
  Keywords: wemo,api
8
8
  Author: Eric Severance
9
9
  Author-email: pywemo@esev.com
10
- Requires-Python: >=3.8.1,<4.0.0
10
+ Requires-Python: >=3.10.0
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
16
18
  Requires-Dist: ifaddr (>=0.1.0)
17
- Requires-Dist: lxml (>=4.6,<5.0)
19
+ Requires-Dist: lxml (>=4.6)
18
20
  Requires-Dist: requests (>=2.0)
19
- Requires-Dist: urllib3 (>=1.26.0)
21
+ Requires-Dist: urllib3 (>=2.0.2)
22
+ Project-URL: Homepage, https://github.com/pywemo/pywemo
20
23
  Project-URL: Repository, https://github.com/pywemo/pywemo
21
24
  Description-Content-Type: text/x-rst
22
25
 
@@ -70,7 +73,7 @@ After connecting, if the ``pywemo.discover_devices()`` doesn't work, you can get
70
73
 
71
74
  $ arp -a
72
75
  _gateway (10.22.22.1) at [MAC ADDRESS REMOVED] [ether]
73
-
76
+
74
77
  .. code-block:: python
75
78
 
76
79
  >>> import pywemo
@@ -81,8 +84,6 @@ After connecting, if the ``pywemo.discover_devices()`` doesn't work, you can get
81
84
  >>> device.setup(ssid='MY SSID', password='MY NETWORK PASSWORD')
82
85
  ('1', 'success')
83
86
 
84
-
85
-
86
87
  Testing new products
87
88
  --------------------
88
89
  If both methods above are not successful, then ``pywemo`` may not support your WeMo product yet.
@@ -95,6 +96,7 @@ Device Reset and Setup
95
96
  ----------------------
96
97
  PyWeMo includes the ability to reset and setup devices, without using the Belkin app or needing to create a Belkin account.
97
98
  This can be particularly useful if the intended use is fully local control, such as using Home Assistant.
99
+ PyWeMo does not connect nor use the Belkin cloud for any functionality.
98
100
 
99
101
  Reset
100
102
  ~~~~~
@@ -115,17 +117,43 @@ Setup
115
117
 
116
118
  Device setup is through the ``setup`` method, which has two required arguments: ``ssid`` and ``password``.
117
119
  The user must first connect to the devices locally broadcast access point, which typically starts with "WeMo.", and then discover the device there.
118
- Once done, pass the desired SSID and password (WPA2/AES encryption only) to the ``setup`` method to connect it to your wifi network.
120
+ Once done, pass the desired SSID and password (WPA2/AES encryption only) to the ``setup`` method to connect it to your Wi-Fi network.
119
121
 
120
- ``device.setup(ssid='wifi_name', password='special_secret')``
122
+ ``device.setup(ssid='wifi', password='secret')``
121
123
 
122
124
  A few important notes:
123
125
 
124
- - Not all devices are currently supported for setup.
125
- - For a WeMo without internet access, see `this guide <https://github.com/pywemo/pywemo/wiki/WeMo-Cloud#disconnecting-from-the-cloud>`_ to stop any blinking lights.
126
126
  - If connecting to an open network, the password argument is ignored and you can provide anything, e.g. ``password=None``.
127
- - If connecting to a WPA2/AES-encrypted network, OpenSSL is used to encrypt the password by the ``pywemo`` library.
127
+ - If connecting to a WPA2/AES/TKIPAES-encrypted network, OpenSSL is used to encrypt the password by the ``pywemo`` library.
128
128
  It must be installed and available on your ``PATH`` via calling ``openssl`` from a terminal or command prompt.
129
+ - For a WeMo without internet access, see `this guide <https://github.com/pywemo/pywemo/wiki/WeMo-Cloud#disconnecting-from-the-cloud>`_ to stop any blinking lights.
130
+
131
+ If you have issues connecting, here are several things worth trying:
132
+
133
+ - Try again!
134
+ WeMo devices sometimes just fail to connect and repeating the exact same steps may subsequently work.
135
+ - Bring the WeMo as close to the access point as possible.
136
+ Some devices seem to require a very strong signal for setup, even if they will work normally with a weaker one.
137
+ - WeMo devices can only connect to 2.4GHz Wi-Fi and sometimes have trouble connecting if the 2.4Ghz and 5Ghz SSID are the same.
138
+ - If issues persist, consider performing a full factory reset and power cycle on the device before trying again.
139
+ - Enabled firewall rules may block the WeMo from connecting to the intended AP.
140
+ - Based on various differences in models and firmware, pywemo contains 3 different methods for encrypting the Wi-Fi password when sending it to the WeMo device.
141
+ In addition to the encryption, WeMo devices sometimes expect the get password lengths appended to the end of the password.
142
+ There is logic in pywemo that attempts to select the appropriate options for each device, but it maybe not be correct for all devices and firmware.
143
+ Thus, you may want to try forcing each of the 6 possible combinations as shown below.
144
+ If one of these other methods work, but not the automatic detection, then be sure to add a comment to `this pywemo issue`_.
145
+
146
+ .. code-block:: python
147
+
148
+ device.setup(ssid='wifi', password='secret', _encrypt_method=1, _add_password_lengths=True)
149
+ device.setup(ssid='wifi', password='secret', _encrypt_method=2, _add_password_lengths=False)
150
+ device.setup(ssid='wifi', password='secret', _encrypt_method=3, _add_password_lengths=True)
151
+ # only the top 3 should be valid, but go ahead and try these lower 3 too...
152
+ device.setup(ssid='wifi', password='secret', _encrypt_method=1, _add_password_lengths=False)
153
+ device.setup(ssid='wifi', password='secret', _encrypt_method=2, _add_password_lengths=True)
154
+ device.setup(ssid='wifi', password='secret', _encrypt_method=3, _add_password_lengths=False)
155
+
156
+ Search for your device on `this pywemo issue`_ before opening a new issue if setup does not work for your device.
129
157
 
130
158
  Firmware Warning
131
159
  ----------------
@@ -136,6 +164,62 @@ This raises the possibility that Belkin could, in the future, update WeMo device
136
164
  If this happens, ``pywemo`` may no longer function on that device.
137
165
  Thus it would be prudent to upgrade firmware cautiously and preferably only after confirming that breaking API changes have not been introduced.
138
166
 
167
+ Belkin Ends Support for WeMo
168
+ ----------------------------
169
+ Note that Belkin is officially ending WeMo support on January 31, 2026.
170
+ After this date, the Belkin app will no longer work, including the required cloud access to use the current products.
171
+ This also means that you cannot use the Belkin app to connect a device to your network after this date either.
172
+ See `this link <https://www.belkin.com/support-article/?articleNum=335419>`_ for more details from Belkin.
173
+
174
+ The good news is that this change will **not** affect pywemo, which will continue to work as it currently does;
175
+ pywemo does not rely on the cloud connection for anything, including setup.
176
+ Many products can be setup and reset with pywemo, as discussed above.
177
+
178
+ Please see `this pywemo issue`_ to document the status of the various products and to update the table below on product status.
179
+
180
+ Product Status
181
+ --------------
182
+ This is a list of known products and the pywemo status of each, including for setup.
183
+ This list was started in November of 2025 in response to Belkin ending WeMo support.
184
+ Any entry with N/A is unreported since this table was added.
185
+ If you have any of these decvices and use them with PyWeMo, please let us know in `this pywemo issue`_ so that we can complete this list.
186
+
187
+ This list is mostly from the Belkin article mentioned above, but it may not be a complete list of all products.
188
+ SKU's with an asterisk at the end, like F7C029V2*, are not listed in the article.
189
+
190
+ ========= ======================================= ==================== =================== ========================================
191
+ SKU's Description PyWeMo Object PyWeMo Setup Status Known Working Firmware(s)
192
+ ========= ======================================= ==================== =================== ========================================
193
+ F7C031 Wemo Link Bridge N/A N/A
194
+ F7C046 Wemo Humidifier Humidifier N/A N/A
195
+ F7C045 Wemo CrockPot CrockPot N/A N/A
196
+ F7C048 Wemo Heater B N/A N/A N/A
197
+ F7C049 Wemo Air Purifier N/A N/A N/A
198
+ F7C047 Wemo Heater A N/A N/A N/A
199
+ F7C050 Wemo Coffee Maker (Mr. Coffee) CoffeeMaker N/A N/A
200
+ F8J007 Wi-Fi Baby Monitor N/A N/A N/A
201
+ F5Z0489 Wemo LED Lighting Bundle N/A N/A N/A
202
+ F7C028 Wemo Motion Sensor Motion N/A N/A
203
+ F5Z0340 Wemo Switch + Motion Sensor N/A N/A N/A
204
+ F7C043 Wemo Maker Module Maker Works WeMo_WW_2.00.11423.PVT-OWRT-Maker
205
+ F7C033 Wemo Zigbee Bulb, E27 N/A N/A N/A
206
+ F7C061 Wemo Insight v2 N/A N/A N/A
207
+ F7C027 Wemo Switch Switch Works WeMo_WW_2.00.11851.PVT-OWRT-SNS
208
+ F7C062 Wemo Light Switch v2 N/A N/A N/A
209
+ F7C029 Wemo Insight Insight Works WeMo_WW_2.00.11483.PVT-OWRT-Insight
210
+ F7C029V2* Wemo Insight V2 Insight Works WeMo_WW_2.00.10062.PVT-OWRT-InsightV2
211
+ WLS0403 Wemo Smart Light Switch 3-Way LightSwitchLongPress N/A N/A
212
+ WSP070 Wemo Mini Smart Plug N/A N/A N/A
213
+ WDS060 Wemo Wi-Fi Smart Light Switch w/ Dimmer DimmerV2 N/A WEMO_WW_2.00.20110904.PVT-RTOS-DimmerV2
214
+ WLS040 Wemo Smart Light Switch LightSwitchLongPress N/A N/A
215
+ F7C064 Wemo HomeKit N/A N/A N/A
216
+ F7C059 Wemo Dimmer Light Switch DimmerLongPress Works WeMo_WW_2.00.11453.PVT-OWRT-Dimmer
217
+ F7C063 Wemo Mini Plugin Switch Switch Works WeMo_WW_2.00.11452.PVT-OWRT-SNSV2
218
+ F7C030 Wemo Light Switch LightSwitchLongPress Works WeMo_WW_2.00.11408.PVT-OWRT-LS
219
+ WSP090 Wemo Outdoor Plug OutdoorPlug Works WEMO_WW_1.00.20081401.PVT-RTOS-OutdoorV1
220
+ WSP080 Wemo Mini Smart Plug Switch Works WEMO_WW_4.00.20101902.PVT-RTOS-SNSV4
221
+ ========= ======================================= ==================== =================== ========================================
222
+
139
223
  Developing
140
224
  ----------
141
225
  Setup and builds are fully automated.
@@ -163,6 +247,7 @@ License
163
247
  All contents of the pywemo/ouimeaux_device directory are licensed under a BSD 3-Clause license. The full text of that license is maintained within the pywemo/ouimeaux_device/LICENSE file.
164
248
  The rest of pyWeMo is released under the MIT license. See the top-level LICENSE file for more details.
165
249
 
250
+ .. _this pywemo issue: https://github.com/pywemo/pywemo/issues/773
166
251
 
167
252
  .. |Build Badge| image:: https://github.com/pywemo/pywemo/workflows/Build/badge.svg
168
253
  :target: https://github.com/pywemo/pywemo/actions?query=workflow%3ABuild
@@ -0,0 +1,40 @@
1
+ pywemo/README.md,sha256=R8Wukt5JbfA-mORbvI_gyk1INAQ556XTLvOSeu3P3e4,2889
2
+ pywemo/__init__.py,sha256=Rox8IsAVzl2d0csAcyBasrZ46ipoyCjBkxS14OXc4IU,1235
3
+ pywemo/color.py,sha256=QxysaipcmMvZrVeXPgta1L44pqIgIa3qshVadgOJLMw,2842
4
+ pywemo/discovery.py,sha256=yZAz_21VH5XHBsiRk1kYz3wiFqJ7MfYelngxr5ECf18,6807
5
+ pywemo/exceptions.py,sha256=ZsTScQm4zncHUEwImutJFQsRNipji4aEbpTRZLlMFd4,2760
6
+ pywemo/ouimeaux_device/LICENSE,sha256=Ff3shOWJ0P7xPBq6BsFxkORe6BP2iaWj5ydiE0s7B6k,1468
7
+ pywemo/ouimeaux_device/__init__.py,sha256=dJIOgoHoWFV5q9PLnbb67rJWEVBfpS3Ie1eKus2MnX4,31061
8
+ pywemo/ouimeaux_device/api/__init__.py,sha256=TYTMhVymF43pAOH1M1oHZ-7El7G6tTAGBIMrfEGeYOs,23
9
+ pywemo/ouimeaux_device/api/attributes.py,sha256=intKsoptzQGzfYqjYM2BS9k5TiHCIFNLaUX9sqCLOQo,4844
10
+ pywemo/ouimeaux_device/api/db_orm.py,sha256=1qPUem0KPizUwRocyKK-gz578Da-ZA9kLkFgN5ZD_Jk,6731
11
+ pywemo/ouimeaux_device/api/long_press.py,sha256=5xFfYkL9u9P0KCbAKXCQolbM2dF8wirmyMVMsuT6iuw,6087
12
+ pywemo/ouimeaux_device/api/rules_db.py,sha256=BCTNaC6Cbx7wxTCIC4rI8vyoLSmxmv3-VpnDAr2z7b4,14984
13
+ pywemo/ouimeaux_device/api/service.py,sha256=QMFZf6Z5vMOtuX-YalrMeYy8pWjXdntvQ0e__fEoYGU,12734
14
+ pywemo/ouimeaux_device/api/wemo_services.py,sha256=z5wbX10ok2ltdpucJr0H6Cy9CoIoCb3vfbkeMdWt8qc,912
15
+ pywemo/ouimeaux_device/api/wemo_services.pyi,sha256=jnQIl-1HUgQ7cSR3hNMh5hflBaVBWCDk53alL0TccX8,6885
16
+ pywemo/ouimeaux_device/api/xsd/__init__.py,sha256=xv1RKXPNighlx3L88tkedXQfSDYI5ifA8zPmSiXdiyY,60
17
+ pywemo/ouimeaux_device/api/xsd/device.py,sha256=XNpjD0vhLdIFLOs9hurXm9-1m4QattqhDaoFyK7l78w,126869
18
+ pywemo/ouimeaux_device/api/xsd/device.xsd,sha256=D4yqFUD-kDXUXWz3Y2No_XTiNRY2TbxecjV5wEbh5Gw,4305
19
+ pywemo/ouimeaux_device/api/xsd/service.py,sha256=aM2iWYRyTIXOpGKf7tgoAsBGfzoQeZ0Umvt6sStEvms,122561
20
+ pywemo/ouimeaux_device/api/xsd/service.xsd,sha256=Hqzc1ovP7q8SbW-IDN7ZaLQCN_vP50Qrqx7PDAbDAyQ,3526
21
+ pywemo/ouimeaux_device/api/xsd_types.py,sha256=o-5zhJAGWA7DvIhF3t_zzjge8bzA_wvw3YTiw2izvOs,7276
22
+ pywemo/ouimeaux_device/bridge.py,sha256=R1d-CTt238H2eSW04nG_VsQnb20v_2I0_TleNZNZm64,19103
23
+ pywemo/ouimeaux_device/coffeemaker.py,sha256=7r4Vjd9NIwqtc_u1rUcTBYk3md1qpBcSkIJCF9_UzZE,3346
24
+ pywemo/ouimeaux_device/crockpot.py,sha256=3ORa5acc_FNThXi9ok0jZUc40N-pMXBGAgQ4y8Uz81E,5036
25
+ pywemo/ouimeaux_device/dimmer.py,sha256=OaSyjt69_lqKNRSS_ZT7vBXNCeMcZbyPjPv-W-y09fs,2399
26
+ pywemo/ouimeaux_device/humidifier.py,sha256=ZwDfAzU301ZO9z_eCXaEd8DT_rAl5N3zHWa9bNOmLpw,7164
27
+ pywemo/ouimeaux_device/insight.py,sha256=xPQjJO1SiVLdB2rcosOMo-QgUsVcHTmQtI8CoSgng1s,6126
28
+ pywemo/ouimeaux_device/lightswitch.py,sha256=LDYbm3r7q7otau0I8ZfOZWaYAe0tZUPny6Bge9dlGP0,329
29
+ pywemo/ouimeaux_device/maker.py,sha256=KSj-a-4eXQ2fwAt0IKHBdaCUDvdrxV24NMLpOy5CzPQ,1646
30
+ pywemo/ouimeaux_device/motion.py,sha256=yr1JE9Ers7MieNLYJ7vaCRBiuWO11ZFwEXawH1fgWe0,142
31
+ pywemo/ouimeaux_device/outdoor_plug.py,sha256=g5N6PY7NrkIi4TI5YYhPc412R8saQLZW9g22Ec0Iq-U,158
32
+ pywemo/ouimeaux_device/switch.py,sha256=NxIyLhFu92DuYFf0kdFEOUAkBL0xK_5CxntAin2c1nY,1024
33
+ pywemo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ pywemo/ssdp.py,sha256=amAc2w0I8lNQ9THHlwOUOTraxSaNvf_KaLZ5iXO7Wec,12459
35
+ pywemo/subscribe.py,sha256=b4_8CxT3AJPQVwn-bJhQJttiPMlMQMLGYVRFTibusQE,29752
36
+ pywemo/util.py,sha256=yHGVZDpGvMYjz5z4F3jGrcot3XA8o28oMBS-JqemaVI,4434
37
+ pywemo-2.0.0.dist-info/METADATA,sha256=MfWxbLXdCjAww3kttPQGoQqUhQX57MpEFqkUdd5qvUA,16486
38
+ pywemo-2.0.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
39
+ pywemo-2.0.0.dist-info/licenses/LICENSE,sha256=uEhWS0rrkJ-nVSDY4avaxJrszZOBwR_hIOiBVkd4Sk8,2831
40
+ pywemo-2.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.7.0
2
+ Generator: poetry-core 2.3.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,40 +0,0 @@
1
- pywemo/README.md,sha256=R8Wukt5JbfA-mORbvI_gyk1INAQ556XTLvOSeu3P3e4,2889
2
- pywemo/__init__.py,sha256=EF4qCw0GNmNsWV6MZNr1eeArx6OvvDy-akB_dRqsuZg,1254
3
- pywemo/color.py,sha256=eGQd0MODHV_tBCDW9BUDQ_MLvmR0k0MkhIj_s38bfuE,2762
4
- pywemo/discovery.py,sha256=1yI-QRrOQL0_-h-UoNv9huZ6obcMMCBZOp_NJcV2rNk,6806
5
- pywemo/exceptions.py,sha256=WY5iIf-JRfu9TzOqKa7vsGspqakY3UryYn6ugLlEDvQ,2759
6
- pywemo/ouimeaux_device/LICENSE,sha256=Ff3shOWJ0P7xPBq6BsFxkORe6BP2iaWj5ydiE0s7B6k,1468
7
- pywemo/ouimeaux_device/__init__.py,sha256=75NxqCVOX_0iRTGllGo-J7xfHhgP8EPdIjzjDtQniXg,26370
8
- pywemo/ouimeaux_device/api/__init__.py,sha256=TYTMhVymF43pAOH1M1oHZ-7El7G6tTAGBIMrfEGeYOs,23
9
- pywemo/ouimeaux_device/api/attributes.py,sha256=SxfqNOlCOMP4DrlrK5o0T5NApDq_p-Y5h3m99F2EcZU,4843
10
- pywemo/ouimeaux_device/api/db_orm.py,sha256=ODLZZZ6Nfz2OWrKcCVucDml8eVQy-uwHV-P6k4VBW2U,6733
11
- pywemo/ouimeaux_device/api/long_press.py,sha256=GM6Eg8lb7rVTxGgdkV2VvrPf9BK7KIvHthILeyrOvTc,6059
12
- pywemo/ouimeaux_device/api/rules_db.py,sha256=1CugXrlg6zHN07ebhK5e68DYUTPxRStf_EfNGpoY_JE,14976
13
- pywemo/ouimeaux_device/api/service.py,sha256=qeNThZXVPEuRIM9Xou8aVk34Up8F1ei1O6E_X8_FCMo,12705
14
- pywemo/ouimeaux_device/api/wemo_services.py,sha256=z5wbX10ok2ltdpucJr0H6Cy9CoIoCb3vfbkeMdWt8qc,912
15
- pywemo/ouimeaux_device/api/wemo_services.pyi,sha256=Amybn9pfO-EJUge6gYPeADw-_iQYmqfP_fSB1mlkpQk,6838
16
- pywemo/ouimeaux_device/api/xsd/__init__.py,sha256=xv1RKXPNighlx3L88tkedXQfSDYI5ifA8zPmSiXdiyY,60
17
- pywemo/ouimeaux_device/api/xsd/device.py,sha256=CFxdcT6s9yVECitIM2AI-63XaUqbc2zx3XF1a1iXjBQ,126897
18
- pywemo/ouimeaux_device/api/xsd/device.xsd,sha256=D4yqFUD-kDXUXWz3Y2No_XTiNRY2TbxecjV5wEbh5Gw,4305
19
- pywemo/ouimeaux_device/api/xsd/service.py,sha256=OE_VA1zAp8boxGQ-9qa9JyMPC3Lw6tq2AGb01alDMis,122589
20
- pywemo/ouimeaux_device/api/xsd/service.xsd,sha256=Hqzc1ovP7q8SbW-IDN7ZaLQCN_vP50Qrqx7PDAbDAyQ,3526
21
- pywemo/ouimeaux_device/api/xsd_types.py,sha256=pUzyktm26SK6nnwAJYVINkYx4tLFE0MDK4lOaLIOZ5A,7275
22
- pywemo/ouimeaux_device/bridge.py,sha256=yI_JCCqyO6Ye_Ol3wDtBuvVUuX82V4c1StnFEsqzMd8,18975
23
- pywemo/ouimeaux_device/coffeemaker.py,sha256=B80yKYZCJKr9tC-Zf7Bc4YlHYj74jAq2GLANsL_TqQo,3345
24
- pywemo/ouimeaux_device/crockpot.py,sha256=PXPvFRfwNXyvkhkQJl3Hcdf1VEpazJ2DGOrfa5WGKpQ,5035
25
- pywemo/ouimeaux_device/dimmer.py,sha256=k_GJIcMygOZ2VjzSlJslYh3T0X3BxyakcyNecvi4gA8,2398
26
- pywemo/ouimeaux_device/humidifier.py,sha256=dnqwTOlWun1crsBR8E5H-oMF0851qz2K_o3cH6qVDhs,7162
27
- pywemo/ouimeaux_device/insight.py,sha256=EVcFWZN2UVNj4vA4xrDk7FdbVvnELSdKg9OJ200BT4w,6125
28
- pywemo/ouimeaux_device/lightswitch.py,sha256=_lbaLmxPIKhWLmjEhZ6NBSzFHDvg9sW0Z3Dn4PiO5mw,328
29
- pywemo/ouimeaux_device/maker.py,sha256=azcPLnFBJRDgjAEV6oVVdaOGGg0JMLaPztuGtTafnNw,1645
30
- pywemo/ouimeaux_device/motion.py,sha256=ZepSPKKZxo8HPZ391z7SHIxmZXdV5vrEJBfac3z1NHg,141
31
- pywemo/ouimeaux_device/outdoor_plug.py,sha256=M8AmM_eaU43JaIPF1F-NGoY-zaE99JQucNs_BOCC-DU,157
32
- pywemo/ouimeaux_device/switch.py,sha256=VMLyoXRwOIFbjnwR2GcFRzwkzDNqVawu5z3f_9M7cB8,1023
33
- pywemo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- pywemo/ssdp.py,sha256=koDFc8INDuWzPNWftnfCOpSUz4t3Q9giB6GeSbMOZVw,12531
35
- pywemo/subscribe.py,sha256=j_zbbgnWZZRPSE5fmpsX1sIPGbCxzGQmNIiI1GZ_dvA,29780
36
- pywemo/util.py,sha256=LHOD-MSIkl78jElP5uFnZVJ9dBovhYQ_2Gx2JRlbgxs,4417
37
- pywemo-1.3.1.dist-info/LICENSE,sha256=uEhWS0rrkJ-nVSDY4avaxJrszZOBwR_hIOiBVkd4Sk8,2831
38
- pywemo-1.3.1.dist-info/METADATA,sha256=YpeYKC4GnPfxuERaj4FclOLWzI9X1e3yTHE_WpYbuoI,9084
39
- pywemo-1.3.1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
40
- pywemo-1.3.1.dist-info/RECORD,,