pywemo 1.3.0__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 +1 -1
- pywemo/color.py +14 -13
- pywemo/discovery.py +1 -0
- pywemo/exceptions.py +1 -0
- pywemo/ouimeaux_device/__init__.py +152 -25
- pywemo/ouimeaux_device/api/attributes.py +1 -0
- pywemo/ouimeaux_device/api/db_orm.py +3 -1
- pywemo/ouimeaux_device/api/long_press.py +3 -1
- pywemo/ouimeaux_device/api/rules_db.py +5 -4
- pywemo/ouimeaux_device/api/service.py +10 -7
- pywemo/ouimeaux_device/api/wemo_services.pyi +4 -3
- pywemo/ouimeaux_device/api/xsd/device.py +10 -11
- pywemo/ouimeaux_device/api/xsd/service.py +10 -11
- pywemo/ouimeaux_device/api/xsd_types.py +1 -0
- pywemo/ouimeaux_device/bridge.py +10 -4
- pywemo/ouimeaux_device/coffeemaker.py +1 -0
- pywemo/ouimeaux_device/crockpot.py +1 -0
- pywemo/ouimeaux_device/dimmer.py +5 -0
- pywemo/ouimeaux_device/humidifier.py +2 -0
- pywemo/ouimeaux_device/insight.py +1 -0
- pywemo/ouimeaux_device/lightswitch.py +1 -0
- pywemo/ouimeaux_device/maker.py +1 -0
- pywemo/ouimeaux_device/motion.py +1 -0
- pywemo/ouimeaux_device/outdoor_plug.py +1 -0
- pywemo/ouimeaux_device/switch.py +1 -0
- pywemo/ssdp.py +4 -5
- pywemo/subscribe.py +9 -5
- pywemo/util.py +4 -4
- {pywemo-1.3.0.dist-info → pywemo-2.0.0.dist-info}/METADATA +100 -15
- pywemo-2.0.0.dist-info/RECORD +40 -0
- {pywemo-1.3.0.dist-info → pywemo-2.0.0.dist-info}/WHEEL +1 -1
- pywemo-1.3.0.dist-info/RECORD +0 -40
- {pywemo-1.3.0.dist-info → pywemo-2.0.0.dist-info/licenses}/LICENSE +0 -0
pywemo/__init__.py
CHANGED
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] =
|
|
10
|
-
|
|
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] =
|
|
21
|
-
|
|
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
pywemo/exceptions.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
328
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
"""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}={
|
|
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
|
|
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,
|
|
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:
|
|
277
|
-
rule_type:
|
|
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:
|
|
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
|
|
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
|
-
"""
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
pywemo/ouimeaux_device/bridge.py
CHANGED
|
@@ -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,
|
|
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(
|
|
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}: {
|
|
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: {
|
|
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
|
pywemo/ouimeaux_device/dimmer.py
CHANGED
|
@@ -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
|
|
@@ -38,6 +39,10 @@ class Dimmer(Switch):
|
|
|
38
39
|
|
|
39
40
|
def get_state(self, force_update: bool = False) -> int:
|
|
40
41
|
"""Update the state & brightness for the Dimmer."""
|
|
42
|
+
force_update = force_update or (
|
|
43
|
+
self._brightness is None
|
|
44
|
+
and "brightness" not in self.basic_state_params
|
|
45
|
+
)
|
|
41
46
|
state = super().get_state(force_update)
|
|
42
47
|
if force_update or self._brightness is None:
|
|
43
48
|
try:
|
|
@@ -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
|
|
pywemo/ouimeaux_device/maker.py
CHANGED
pywemo/ouimeaux_device/motion.py
CHANGED
pywemo/ouimeaux_device/switch.py
CHANGED
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 = ...
|
|
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(
|
|
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.
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pywemo
|
|
3
|
-
Version:
|
|
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.
|
|
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
|
|
19
|
+
Requires-Dist: lxml (>=4.6)
|
|
18
20
|
Requires-Dist: requests (>=2.0)
|
|
19
|
-
Requires-Dist: urllib3 (>=
|
|
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
|
|
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='
|
|
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,,
|
pywemo-1.3.0.dist-info/RECORD
DELETED
|
@@ -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=swTGcPmzqQfyhwDNiWLFh-yNw4PKw0BQURS4Q-03kvY,126897
|
|
18
|
-
pywemo/ouimeaux_device/api/xsd/device.xsd,sha256=D4yqFUD-kDXUXWz3Y2No_XTiNRY2TbxecjV5wEbh5Gw,4305
|
|
19
|
-
pywemo/ouimeaux_device/api/xsd/service.py,sha256=NyU7RXfUmW-XAhjHmVUpwB2Gu9bsg41LdA3J99mCCNw,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=RVAp6G_9K6zyVR_pLtadmoSMCFxI2WK2aME99LEOf7U,2250
|
|
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.0.dist-info/LICENSE,sha256=uEhWS0rrkJ-nVSDY4avaxJrszZOBwR_hIOiBVkd4Sk8,2831
|
|
38
|
-
pywemo-1.3.0.dist-info/METADATA,sha256=gGP2lPlXl1pLBw_dfv7vi9B7tVkB8EDajvaDNgA8SqM,9084
|
|
39
|
-
pywemo-1.3.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
|
|
40
|
-
pywemo-1.3.0.dist-info/RECORD,,
|
|
File without changes
|