pywemo 1.2.1__py3-none-any.whl → 1.3.1__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.
Potentially problematic release.
This version of pywemo might be problematic. Click here for more details.
- pywemo/ouimeaux_device/api/xsd/device.py +10 -4
- pywemo/ouimeaux_device/api/xsd/service.py +10 -4
- pywemo/ouimeaux_device/dimmer.py +4 -0
- pywemo/subscribe.py +62 -27
- {pywemo-1.2.1.dist-info → pywemo-1.3.1.dist-info}/METADATA +1 -1
- {pywemo-1.2.1.dist-info → pywemo-1.3.1.dist-info}/RECORD +8 -8
- {pywemo-1.2.1.dist-info → pywemo-1.3.1.dist-info}/WHEEL +1 -1
- {pywemo-1.2.1.dist-info → pywemo-1.3.1.dist-info}/LICENSE +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# pylint: skip-file
|
|
7
7
|
|
|
8
8
|
#
|
|
9
|
-
# Generated by generateDS.py version 2.
|
|
9
|
+
# Generated by generateDS.py version 2.43.2.
|
|
10
10
|
# Python [sys.version]
|
|
11
11
|
#
|
|
12
12
|
# Command line options:
|
|
@@ -1219,10 +1219,11 @@ def _cast(typ, value):
|
|
|
1219
1219
|
|
|
1220
1220
|
|
|
1221
1221
|
#
|
|
1222
|
-
#
|
|
1222
|
+
# Start enum classes
|
|
1223
|
+
#
|
|
1224
|
+
#
|
|
1225
|
+
# Start data representation classes
|
|
1223
1226
|
#
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
1227
|
class root(GeneratedsSuper):
|
|
1227
1228
|
__hash__ = GeneratedsSuper.__hash__
|
|
1228
1229
|
subclass = None
|
|
@@ -3635,6 +3636,11 @@ class serviceType(GeneratedsSuper):
|
|
|
3635
3636
|
# end class serviceType
|
|
3636
3637
|
|
|
3637
3638
|
|
|
3639
|
+
#
|
|
3640
|
+
# End data representation classes.
|
|
3641
|
+
#
|
|
3642
|
+
|
|
3643
|
+
|
|
3638
3644
|
GDSClassesMapping = {}
|
|
3639
3645
|
|
|
3640
3646
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# pylint: skip-file
|
|
7
7
|
|
|
8
8
|
#
|
|
9
|
-
# Generated by generateDS.py version 2.
|
|
9
|
+
# Generated by generateDS.py version 2.43.2.
|
|
10
10
|
# Python [sys.version]
|
|
11
11
|
#
|
|
12
12
|
# Command line options:
|
|
@@ -1219,10 +1219,11 @@ def _cast(typ, value):
|
|
|
1219
1219
|
|
|
1220
1220
|
|
|
1221
1221
|
#
|
|
1222
|
-
#
|
|
1222
|
+
# Start enum classes
|
|
1223
|
+
#
|
|
1224
|
+
#
|
|
1225
|
+
# Start data representation classes
|
|
1223
1226
|
#
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
1227
|
class scpd(GeneratedsSuper):
|
|
1227
1228
|
__hash__ = GeneratedsSuper.__hash__
|
|
1228
1229
|
subclass = None
|
|
@@ -3593,6 +3594,11 @@ class retvalType(GeneratedsSuper):
|
|
|
3593
3594
|
# end class retvalType
|
|
3594
3595
|
|
|
3595
3596
|
|
|
3597
|
+
#
|
|
3598
|
+
# End data representation classes.
|
|
3599
|
+
#
|
|
3600
|
+
|
|
3601
|
+
|
|
3596
3602
|
GDSClassesMapping = {}
|
|
3597
3603
|
|
|
3598
3604
|
|
pywemo/ouimeaux_device/dimmer.py
CHANGED
|
@@ -38,6 +38,10 @@ class Dimmer(Switch):
|
|
|
38
38
|
|
|
39
39
|
def get_state(self, force_update: bool = False) -> int:
|
|
40
40
|
"""Update the state & brightness for the Dimmer."""
|
|
41
|
+
force_update = force_update or (
|
|
42
|
+
self._brightness is None
|
|
43
|
+
and "brightness" not in self.basic_state_params
|
|
44
|
+
)
|
|
41
45
|
state = super().get_state(force_update)
|
|
42
46
|
if force_update or self._brightness is None:
|
|
43
47
|
try:
|
pywemo/subscribe.py
CHANGED
|
@@ -37,8 +37,10 @@ import collections
|
|
|
37
37
|
import logging
|
|
38
38
|
import os
|
|
39
39
|
import sched
|
|
40
|
+
import secrets
|
|
40
41
|
import threading
|
|
41
42
|
import time
|
|
43
|
+
import warnings
|
|
42
44
|
from collections.abc import Iterable, MutableMapping
|
|
43
45
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
44
46
|
from typing import Any, Callable
|
|
@@ -140,7 +142,7 @@ class Subscription:
|
|
|
140
142
|
scheduler_active: bool = True
|
|
141
143
|
"""
|
|
142
144
|
Controls whether or not the subscription will continue to be periodically
|
|
143
|
-
scheduled by the Scheduler. Set to False when the device
|
|
145
|
+
scheduled by the Scheduler. Set to False when the device is unregistered.
|
|
144
146
|
"""
|
|
145
147
|
|
|
146
148
|
expiration_time: float = 0.0
|
|
@@ -171,6 +173,9 @@ class Subscription:
|
|
|
171
173
|
service_name: str
|
|
172
174
|
"""Name of the subscription endpoint service."""
|
|
173
175
|
|
|
176
|
+
path: str
|
|
177
|
+
"""Unique path used to for the subscription callback."""
|
|
178
|
+
|
|
174
179
|
def __init__(
|
|
175
180
|
self, device: Device, callback_port: int, service_name: str
|
|
176
181
|
) -> None:
|
|
@@ -178,6 +183,7 @@ class Subscription:
|
|
|
178
183
|
self.device = device
|
|
179
184
|
self.callback_port = callback_port
|
|
180
185
|
self.service_name = service_name
|
|
186
|
+
self.path = f"/sub/{service_name}/{secrets.token_urlsafe(24)}"
|
|
181
187
|
|
|
182
188
|
def __repr__(self) -> str:
|
|
183
189
|
"""Return a string representation of the Subscription."""
|
|
@@ -221,6 +227,16 @@ class Subscription:
|
|
|
221
227
|
|
|
222
228
|
return self._update_subscription(response.headers)
|
|
223
229
|
|
|
230
|
+
def cancel(self) -> None:
|
|
231
|
+
"""Cancel a subscription."""
|
|
232
|
+
if self.expiration_time > time.time():
|
|
233
|
+
try:
|
|
234
|
+
self._unsubscribe()
|
|
235
|
+
except requests.RequestException:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
self._reset_subscription()
|
|
239
|
+
|
|
224
240
|
def _subscribe(self) -> requests.Response:
|
|
225
241
|
"""Start/renew a subscription with a UPnP SUBSCRIBE request.
|
|
226
242
|
|
|
@@ -290,11 +306,6 @@ class Subscription:
|
|
|
290
306
|
"""URL for the UPnP subscription endoint."""
|
|
291
307
|
return self.device.services[self.service_name].eventSubURL
|
|
292
308
|
|
|
293
|
-
@property
|
|
294
|
-
def path(self) -> str:
|
|
295
|
-
"""Path for the callback to disambiguate multiple subscriptions."""
|
|
296
|
-
return f"/sub/{self.service_name}"
|
|
297
|
-
|
|
298
309
|
@property
|
|
299
310
|
def is_subscribed(self) -> bool:
|
|
300
311
|
"""Return True if the subscription is active, False otherwise.
|
|
@@ -346,6 +357,8 @@ def _cancel_events(
|
|
|
346
357
|
# event might execute and be removed from queue
|
|
347
358
|
# concurrently. Safe to ignore
|
|
348
359
|
pass
|
|
360
|
+
if subscription.scheduler_active:
|
|
361
|
+
scheduler.enter(0, 0, subscription.cancel)
|
|
349
362
|
# Prevent the subscription from being scheduled again.
|
|
350
363
|
subscription.scheduler_active = False
|
|
351
364
|
subscription.scheduler_event = None
|
|
@@ -407,7 +420,16 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
|
407
420
|
"""Handle subscription responses received from devices."""
|
|
408
421
|
sender_ip, _ = self.client_address
|
|
409
422
|
outer = self.server.outer
|
|
410
|
-
|
|
423
|
+
# Security consideration: Given that the subscription paths are
|
|
424
|
+
# randomized, I considered removing the host/IP check below. However,
|
|
425
|
+
# since these requests are not encrypted, it is possible for someone
|
|
426
|
+
# to observe the random URL path. I therefore have kept the host/IP
|
|
427
|
+
# check as a defense-in-depth strategy for preventing the device state
|
|
428
|
+
# from being changed by someone who could observe the http requests.
|
|
429
|
+
if (
|
|
430
|
+
# pylint: disable=protected-access
|
|
431
|
+
subscription := outer._subscription_paths.get(self.path)
|
|
432
|
+
) is None or subscription.device.host != sender_ip:
|
|
411
433
|
LOG.warning(
|
|
412
434
|
"Received %s event for unregistered device %s",
|
|
413
435
|
self.path,
|
|
@@ -418,7 +440,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
|
418
440
|
for propnode in doc.findall(f"./{NS}property"):
|
|
419
441
|
for property_ in list(propnode):
|
|
420
442
|
outer.event(
|
|
421
|
-
device,
|
|
443
|
+
subscription.device,
|
|
422
444
|
property_.tag,
|
|
423
445
|
property_.text or "",
|
|
424
446
|
path=self.path,
|
|
@@ -440,14 +462,19 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
|
440
462
|
if self.path.endswith("/upnp/control/basicevent1"):
|
|
441
463
|
sender_ip, _ = self.client_address
|
|
442
464
|
outer = self.server.outer
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
465
|
+
for (
|
|
466
|
+
device
|
|
467
|
+
) in outer._subscriptions: # pylint: disable=protected-access
|
|
468
|
+
if device.host != sender_ip:
|
|
469
|
+
continue
|
|
448
470
|
doc = self._get_xml_from_http_body()
|
|
449
471
|
if binary_state := doc.findtext(".//BinaryState"):
|
|
450
472
|
outer.event(device, EVENT_TYPE_LONG_PRESS, binary_state)
|
|
473
|
+
break
|
|
474
|
+
else:
|
|
475
|
+
LOG.warning(
|
|
476
|
+
"Received event for unregistered device %s", sender_ip
|
|
477
|
+
)
|
|
451
478
|
action = self.headers.get("SOAPACTION", "")
|
|
452
479
|
response = SOAP_ACTION_RESPONSE.get(
|
|
453
480
|
action, ERROR_SOAP_ACTION_RESPONSE
|
|
@@ -526,7 +553,6 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
526
553
|
|
|
527
554
|
def __init__(self, requested_port: int | None = None) -> None:
|
|
528
555
|
"""Create the subscription registry object."""
|
|
529
|
-
self.devices: dict[str, Device] = {}
|
|
530
556
|
self._callbacks: dict[
|
|
531
557
|
Device, list[tuple[str | None, SubscriberCallback]]
|
|
532
558
|
] = collections.defaultdict(list)
|
|
@@ -535,6 +561,7 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
535
561
|
self._event_thread: threading.Thread | None = None
|
|
536
562
|
self._event_thread_cond = threading.Condition()
|
|
537
563
|
self._subscriptions: dict[Device, list[Subscription]] = {}
|
|
564
|
+
self._subscription_paths: dict[str, Subscription] = {}
|
|
538
565
|
|
|
539
566
|
def sleep(secs: float) -> None:
|
|
540
567
|
with self._event_thread_cond:
|
|
@@ -559,14 +586,13 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
559
586
|
return
|
|
560
587
|
|
|
561
588
|
LOG.info("Subscribing to events from %r", device)
|
|
562
|
-
self.devices[device.host] = device
|
|
563
|
-
|
|
564
589
|
with self._event_thread_cond:
|
|
565
590
|
subscriptions = self._subscriptions[device] = []
|
|
566
591
|
for service in self.subscription_service_names:
|
|
567
592
|
if service in device.services:
|
|
568
593
|
subscription = Subscription(device, self.port, service)
|
|
569
594
|
subscriptions.append(subscription)
|
|
595
|
+
self._subscription_paths[subscription.path] = subscription
|
|
570
596
|
self._schedule(0, subscription)
|
|
571
597
|
self._event_thread_cond.notify()
|
|
572
598
|
|
|
@@ -580,13 +606,11 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
580
606
|
|
|
581
607
|
with self._event_thread_cond:
|
|
582
608
|
# Remove any events, callbacks, and the device itself
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
del self.
|
|
588
|
-
if device.host in self.devices:
|
|
589
|
-
del self.devices[device.host]
|
|
609
|
+
self._callbacks.pop(device, None)
|
|
610
|
+
subscriptions = self._subscriptions.pop(device, [])
|
|
611
|
+
_cancel_events(self._sched, subscriptions)
|
|
612
|
+
for subscription in subscriptions:
|
|
613
|
+
del self._subscription_paths[subscription.path]
|
|
590
614
|
self._event_thread_cond.notify()
|
|
591
615
|
|
|
592
616
|
def _resubscribe(self, subscription: Subscription, retry: int = 0) -> None:
|
|
@@ -646,10 +670,10 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
646
670
|
)
|
|
647
671
|
if path:
|
|
648
672
|
# Update the event_received property for the subscription.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
673
|
+
if (
|
|
674
|
+
subscription := self._subscription_paths.get(path)
|
|
675
|
+
) is not None:
|
|
676
|
+
subscription.event_received = True
|
|
653
677
|
else:
|
|
654
678
|
LOG.warning(
|
|
655
679
|
"Received unexpected subscription path (%s) for device %s",
|
|
@@ -745,3 +769,14 @@ class SubscriptionRegistry: # pylint: disable=too-many-instance-attributes
|
|
|
745
769
|
while not self._exiting and self._sched.empty():
|
|
746
770
|
self._event_thread_cond.wait(10)
|
|
747
771
|
self._sched.run()
|
|
772
|
+
|
|
773
|
+
@property
|
|
774
|
+
def devices(self) -> dict[str, Device]:
|
|
775
|
+
"""Deprecated mapping of IP address to device."""
|
|
776
|
+
warnings.warn(
|
|
777
|
+
"The devices dict is deprecated "
|
|
778
|
+
"and will be removed in a future release.",
|
|
779
|
+
DeprecationWarning,
|
|
780
|
+
stacklevel=1,
|
|
781
|
+
)
|
|
782
|
+
return {device.host: device for device in self._subscriptions}
|
|
@@ -14,15 +14,15 @@ pywemo/ouimeaux_device/api/service.py,sha256=qeNThZXVPEuRIM9Xou8aVk34Up8F1ei1O6E
|
|
|
14
14
|
pywemo/ouimeaux_device/api/wemo_services.py,sha256=z5wbX10ok2ltdpucJr0H6Cy9CoIoCb3vfbkeMdWt8qc,912
|
|
15
15
|
pywemo/ouimeaux_device/api/wemo_services.pyi,sha256=Amybn9pfO-EJUge6gYPeADw-_iQYmqfP_fSB1mlkpQk,6838
|
|
16
16
|
pywemo/ouimeaux_device/api/xsd/__init__.py,sha256=xv1RKXPNighlx3L88tkedXQfSDYI5ifA8zPmSiXdiyY,60
|
|
17
|
-
pywemo/ouimeaux_device/api/xsd/device.py,sha256=
|
|
17
|
+
pywemo/ouimeaux_device/api/xsd/device.py,sha256=CFxdcT6s9yVECitIM2AI-63XaUqbc2zx3XF1a1iXjBQ,126897
|
|
18
18
|
pywemo/ouimeaux_device/api/xsd/device.xsd,sha256=D4yqFUD-kDXUXWz3Y2No_XTiNRY2TbxecjV5wEbh5Gw,4305
|
|
19
|
-
pywemo/ouimeaux_device/api/xsd/service.py,sha256=
|
|
19
|
+
pywemo/ouimeaux_device/api/xsd/service.py,sha256=OE_VA1zAp8boxGQ-9qa9JyMPC3Lw6tq2AGb01alDMis,122589
|
|
20
20
|
pywemo/ouimeaux_device/api/xsd/service.xsd,sha256=Hqzc1ovP7q8SbW-IDN7ZaLQCN_vP50Qrqx7PDAbDAyQ,3526
|
|
21
21
|
pywemo/ouimeaux_device/api/xsd_types.py,sha256=pUzyktm26SK6nnwAJYVINkYx4tLFE0MDK4lOaLIOZ5A,7275
|
|
22
22
|
pywemo/ouimeaux_device/bridge.py,sha256=yI_JCCqyO6Ye_Ol3wDtBuvVUuX82V4c1StnFEsqzMd8,18975
|
|
23
23
|
pywemo/ouimeaux_device/coffeemaker.py,sha256=B80yKYZCJKr9tC-Zf7Bc4YlHYj74jAq2GLANsL_TqQo,3345
|
|
24
24
|
pywemo/ouimeaux_device/crockpot.py,sha256=PXPvFRfwNXyvkhkQJl3Hcdf1VEpazJ2DGOrfa5WGKpQ,5035
|
|
25
|
-
pywemo/ouimeaux_device/dimmer.py,sha256=
|
|
25
|
+
pywemo/ouimeaux_device/dimmer.py,sha256=k_GJIcMygOZ2VjzSlJslYh3T0X3BxyakcyNecvi4gA8,2398
|
|
26
26
|
pywemo/ouimeaux_device/humidifier.py,sha256=dnqwTOlWun1crsBR8E5H-oMF0851qz2K_o3cH6qVDhs,7162
|
|
27
27
|
pywemo/ouimeaux_device/insight.py,sha256=EVcFWZN2UVNj4vA4xrDk7FdbVvnELSdKg9OJ200BT4w,6125
|
|
28
28
|
pywemo/ouimeaux_device/lightswitch.py,sha256=_lbaLmxPIKhWLmjEhZ6NBSzFHDvg9sW0Z3Dn4PiO5mw,328
|
|
@@ -32,9 +32,9 @@ pywemo/ouimeaux_device/outdoor_plug.py,sha256=M8AmM_eaU43JaIPF1F-NGoY-zaE99JQucN
|
|
|
32
32
|
pywemo/ouimeaux_device/switch.py,sha256=VMLyoXRwOIFbjnwR2GcFRzwkzDNqVawu5z3f_9M7cB8,1023
|
|
33
33
|
pywemo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
pywemo/ssdp.py,sha256=koDFc8INDuWzPNWftnfCOpSUz4t3Q9giB6GeSbMOZVw,12531
|
|
35
|
-
pywemo/subscribe.py,sha256=
|
|
35
|
+
pywemo/subscribe.py,sha256=j_zbbgnWZZRPSE5fmpsX1sIPGbCxzGQmNIiI1GZ_dvA,29780
|
|
36
36
|
pywemo/util.py,sha256=LHOD-MSIkl78jElP5uFnZVJ9dBovhYQ_2Gx2JRlbgxs,4417
|
|
37
|
-
pywemo-1.
|
|
38
|
-
pywemo-1.
|
|
39
|
-
pywemo-1.
|
|
40
|
-
pywemo-1.
|
|
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,,
|
|
File without changes
|