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.

@@ -6,7 +6,7 @@
6
6
  # pylint: skip-file
7
7
 
8
8
  #
9
- # Generated by generateDS.py version 2.42.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
- # Data representation classes.
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.42.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
- # Data representation classes.
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
 
@@ -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 us unregistered.
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
- if (device := outer.devices.get(sender_ip)) is None:
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
- if (device := outer.devices.get(sender_ip)) is None:
444
- LOG.warning(
445
- "Received event for unregistered device %s", sender_ip
446
- )
447
- else:
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
- if device in self._callbacks:
584
- del self._callbacks[device]
585
- if device in self._subscriptions:
586
- _cancel_events(self._sched, self._subscriptions[device])
587
- del self._subscriptions[device]
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
- for subscription in self._subscriptions.get(device, []):
650
- if subscription.path == path:
651
- subscription.event_received = True
652
- break
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}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywemo
3
- Version: 1.2.1
3
+ Version: 1.3.1
4
4
  Summary: Lightweight Python module to discover and control WeMo devices
5
5
  Home-page: https://github.com/pywemo/pywemo
6
6
  License: MIT
@@ -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=6NvcCAkYEkG7bZgBtXtxdIxyAxOkbZPpTR-pC0hXDAw,126828
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=SQ_ZhVXxRMILjhq-zTgMOiGN9FFZMR94GDlpbp6TwJo,122520
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=RVAp6G_9K6zyVR_pLtadmoSMCFxI2WK2aME99LEOf7U,2250
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=jOs3NRov_FC5O44ILJ7niVqBFMIwwHB4ScQVJOZp1I4,28315
35
+ pywemo/subscribe.py,sha256=j_zbbgnWZZRPSE5fmpsX1sIPGbCxzGQmNIiI1GZ_dvA,29780
36
36
  pywemo/util.py,sha256=LHOD-MSIkl78jElP5uFnZVJ9dBovhYQ_2Gx2JRlbgxs,4417
37
- pywemo-1.2.1.dist-info/LICENSE,sha256=uEhWS0rrkJ-nVSDY4avaxJrszZOBwR_hIOiBVkd4Sk8,2831
38
- pywemo-1.2.1.dist-info/METADATA,sha256=bLVhIZAdBLB8ZULuZwUklQNPTFq6ntccwoYc4aUuzk4,9084
39
- pywemo-1.2.1.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
40
- pywemo-1.2.1.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.6.1
2
+ Generator: poetry-core 1.7.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any