pywemo 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywemo/README.md +69 -0
- pywemo/__init__.py +33 -0
- pywemo/color.py +79 -0
- pywemo/discovery.py +194 -0
- pywemo/exceptions.py +94 -0
- pywemo/ouimeaux_device/LICENSE +12 -0
- pywemo/ouimeaux_device/__init__.py +679 -0
- pywemo/ouimeaux_device/api/__init__.py +1 -0
- pywemo/ouimeaux_device/api/attributes.py +131 -0
- pywemo/ouimeaux_device/api/db_orm.py +197 -0
- pywemo/ouimeaux_device/api/long_press.py +168 -0
- pywemo/ouimeaux_device/api/rules_db.py +467 -0
- pywemo/ouimeaux_device/api/service.py +363 -0
- pywemo/ouimeaux_device/api/wemo_services.py +25 -0
- pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
- pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
- pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
- pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
- pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
- pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
- pywemo/ouimeaux_device/api/xsd_types.py +222 -0
- pywemo/ouimeaux_device/bridge.py +506 -0
- pywemo/ouimeaux_device/coffeemaker.py +92 -0
- pywemo/ouimeaux_device/crockpot.py +157 -0
- pywemo/ouimeaux_device/dimmer.py +70 -0
- pywemo/ouimeaux_device/humidifier.py +223 -0
- pywemo/ouimeaux_device/insight.py +191 -0
- pywemo/ouimeaux_device/lightswitch.py +11 -0
- pywemo/ouimeaux_device/maker.py +54 -0
- pywemo/ouimeaux_device/motion.py +6 -0
- pywemo/ouimeaux_device/outdoor_plug.py +6 -0
- pywemo/ouimeaux_device/switch.py +32 -0
- pywemo/py.typed +0 -0
- pywemo/ssdp.py +372 -0
- pywemo/subscribe.py +782 -0
- pywemo/util.py +139 -0
- pywemo-1.4.0.dist-info/LICENSE +54 -0
- pywemo-1.4.0.dist-info/METADATA +192 -0
- pywemo-1.4.0.dist-info/RECORD +40 -0
- pywemo-1.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Representation of a WeMo Maker device."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
from .api.attributes import AttributeDevice
|
|
7
|
+
from .api.service import RequiredService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _Attributes(TypedDict, total=False):
|
|
11
|
+
Switch: int
|
|
12
|
+
Sensor: int
|
|
13
|
+
SwitchMode: int
|
|
14
|
+
SensorPresent: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Maker(AttributeDevice):
|
|
18
|
+
"""Representation of a WeMo Maker device."""
|
|
19
|
+
|
|
20
|
+
_state_property = "switch_state" # Required by AttributeDevice.
|
|
21
|
+
_attributes: _Attributes # Required by AttributeDevice.
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def _required_services(self) -> list[RequiredService]:
|
|
25
|
+
return super()._required_services + [
|
|
26
|
+
RequiredService(name="basicevent", actions=["SetBinaryState"]),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def set_state(self, state: int) -> None:
|
|
30
|
+
"""Set the state of this device to on or off."""
|
|
31
|
+
# The Maker has a momentary mode - so it's not safe to assume
|
|
32
|
+
# the state is what you just set, so re-read it from the device
|
|
33
|
+
self.basicevent.SetBinaryState(BinaryState=int(state))
|
|
34
|
+
self.get_state(True)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def switch_state(self) -> int:
|
|
38
|
+
"""Return the state of the switch."""
|
|
39
|
+
return self._attributes["Switch"]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def sensor_state(self) -> int:
|
|
43
|
+
"""Return the state of the sensor."""
|
|
44
|
+
return self._attributes["Sensor"]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def switch_mode(self) -> int:
|
|
48
|
+
"""Return the switch mode of the sensor."""
|
|
49
|
+
return self._attributes["SwitchMode"]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def has_sensor(self) -> int:
|
|
53
|
+
"""Return whether the device has a sensor."""
|
|
54
|
+
return self._attributes["SensorPresent"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Representation of a WeMo Switch device."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import Device
|
|
5
|
+
from .api.service import RequiredService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Switch(Device):
|
|
9
|
+
"""Representation of a WeMo Switch device."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def _required_services(self) -> list[RequiredService]:
|
|
13
|
+
return super()._required_services + [
|
|
14
|
+
RequiredService(name="basicevent", actions=["SetBinaryState"]),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def set_state(self, state: int) -> None:
|
|
18
|
+
"""Set the state of this device to on or off."""
|
|
19
|
+
self.basicevent.SetBinaryState(BinaryState=int(state))
|
|
20
|
+
self._state = int(state)
|
|
21
|
+
|
|
22
|
+
def off(self) -> None:
|
|
23
|
+
"""Turn this device off. If already off, will return "Error"."""
|
|
24
|
+
self.set_state(0)
|
|
25
|
+
|
|
26
|
+
def on(self) -> None: # pylint: disable=invalid-name
|
|
27
|
+
"""Turn this device on. If already on, will return "Error"."""
|
|
28
|
+
self.set_state(1)
|
|
29
|
+
|
|
30
|
+
def toggle(self) -> None:
|
|
31
|
+
"""Toggle the switch's state."""
|
|
32
|
+
self.set_state(not self.get_state())
|
pywemo/py.typed
ADDED
|
File without changes
|
pywemo/ssdp.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Module that implements SSDP protocol."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import select
|
|
7
|
+
import socket
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from wsgiref.handlers import format_date_time
|
|
13
|
+
|
|
14
|
+
from .ouimeaux_device.api.long_press import VIRTUAL_DEVICE_UDN
|
|
15
|
+
from .util import get_callback_address, interface_addresses
|
|
16
|
+
|
|
17
|
+
DISCOVER_TIMEOUT = 5
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
RESPONSE_REGEX = re.compile(r"\n(.*)\: (.*)\r")
|
|
22
|
+
|
|
23
|
+
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=59)
|
|
24
|
+
|
|
25
|
+
MULTICAST_GROUP = "239.255.255.250"
|
|
26
|
+
MULTICAST_PORT = 1900
|
|
27
|
+
|
|
28
|
+
# Wemo specific urn:
|
|
29
|
+
ST = "urn:Belkin:service:basicevent:1"
|
|
30
|
+
VIRTUAL_DEVICE_USN = f"{VIRTUAL_DEVICE_UDN}::{ST}"
|
|
31
|
+
MAX_AGE = 86400
|
|
32
|
+
|
|
33
|
+
SSDP_REPLY = f"""HTTP/1.1 200 OK
|
|
34
|
+
CACHE-CONTROL: max-age={MAX_AGE}
|
|
35
|
+
DATE: %(date)s
|
|
36
|
+
EXT:
|
|
37
|
+
LOCATION: http://%(callback)s/setup.xml
|
|
38
|
+
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
|
|
39
|
+
01-NLS: %(nls)s
|
|
40
|
+
SERVER: Unspecified, UPnP/1.0, Unspecified
|
|
41
|
+
X-User-Agent: pywemo
|
|
42
|
+
ST: {ST}
|
|
43
|
+
USN: {VIRTUAL_DEVICE_USN}
|
|
44
|
+
|
|
45
|
+
""" # Newline characters at the the end of SSDP_REPLY are intentional.
|
|
46
|
+
SSDP_REPLY = SSDP_REPLY.replace("\n", "\r\n")
|
|
47
|
+
|
|
48
|
+
SSDP_NOTIFY = f"""NOTIFY * HTTP/1.1
|
|
49
|
+
HOST: {MULTICAST_GROUP}:{MULTICAST_PORT}
|
|
50
|
+
CACHE-CONTROL: max-age={MAX_AGE}
|
|
51
|
+
LOCATION: http://%(callback)s/setup.xml
|
|
52
|
+
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
|
|
53
|
+
01-NLS: %(nls)s
|
|
54
|
+
NT: {ST}
|
|
55
|
+
NTS: %(nts)s
|
|
56
|
+
SERVER: Unspecified, UPnP/1.0, Unspecified
|
|
57
|
+
X-User-Agent: pywemo
|
|
58
|
+
USN: {VIRTUAL_DEVICE_USN}
|
|
59
|
+
|
|
60
|
+
""" # Newline characters at the the end of SSDP_NOTIFY are intentional.
|
|
61
|
+
SSDP_NOTIFY = SSDP_NOTIFY.replace("\n", "\r\n")
|
|
62
|
+
|
|
63
|
+
EXPECTED_ST_HEADER = ("ST: " + ST).encode("UTF-8")
|
|
64
|
+
EXPECTED_MAN_HEADER = b'MAN: "ssdp:discover"'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class UPNPEntry:
|
|
68
|
+
"""Found uPnP entry."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, values: dict[str, str]) -> None:
|
|
71
|
+
"""Create a UPNPEntry object."""
|
|
72
|
+
self.values = values
|
|
73
|
+
self._created = datetime.now()
|
|
74
|
+
self._expires: datetime | None = None
|
|
75
|
+
|
|
76
|
+
if "cache-control" in self.values:
|
|
77
|
+
cache_seconds = int(self.values["cache-control"].split("=")[1])
|
|
78
|
+
|
|
79
|
+
self._expires = self._created + timedelta(seconds=cache_seconds)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def st(self) -> str | None: # pylint: disable=invalid-name
|
|
83
|
+
"""Return ST value."""
|
|
84
|
+
return self.values.get("st")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def location(self) -> str | None:
|
|
88
|
+
"""Return location value."""
|
|
89
|
+
return self.values.get("location")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def usn(self) -> str | None:
|
|
93
|
+
"""Return unique service name."""
|
|
94
|
+
return self.values.get("usn")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def udn(self) -> str:
|
|
98
|
+
"""Return unique device name."""
|
|
99
|
+
usn = self.usn or ""
|
|
100
|
+
return usn.split("::")[0]
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_response(cls, response: str) -> UPNPEntry:
|
|
104
|
+
"""Create a uPnP entry from a response."""
|
|
105
|
+
return UPNPEntry(
|
|
106
|
+
{
|
|
107
|
+
key.lower(): item
|
|
108
|
+
for key, item in RESPONSE_REGEX.findall(response)
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def _key(self) -> tuple[str, str | None]:
|
|
114
|
+
"""Tuple of values that uniquely identify the UPNPEntry instance."""
|
|
115
|
+
return (self.udn, self.location)
|
|
116
|
+
|
|
117
|
+
def __eq__(self, other: object) -> bool:
|
|
118
|
+
"""Equality operator."""
|
|
119
|
+
return isinstance(other, type(self)) and self._key == other._key
|
|
120
|
+
|
|
121
|
+
def __hash__(self) -> int:
|
|
122
|
+
"""Generate hash of instance."""
|
|
123
|
+
return hash(("UPNPEntry", self._key))
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
"""Return the string representation of the object."""
|
|
127
|
+
st = self.st or "" # pylint: disable=invalid-name
|
|
128
|
+
location = self.location or ""
|
|
129
|
+
udn = self.udn or ""
|
|
130
|
+
return f"<UPNPEntry {st} - {location} - {udn}>"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_ssdp_request(ssdp_st: str, ssdp_mx: int) -> bytes:
|
|
134
|
+
"""Build the standard request to send during SSDP discovery."""
|
|
135
|
+
return "\r\n".join(
|
|
136
|
+
[
|
|
137
|
+
"M-SEARCH * HTTP/1.1",
|
|
138
|
+
f"ST: {ssdp_st}",
|
|
139
|
+
f"MX: {ssdp_mx}",
|
|
140
|
+
'MAN: "ssdp:discover"',
|
|
141
|
+
f"HOST: {MULTICAST_GROUP}:{MULTICAST_PORT}",
|
|
142
|
+
"",
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
).encode("ascii")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def scan( # pylint: disable=too-many-branches,too-many-locals
|
|
149
|
+
st: str = ST, # pylint: disable=invalid-name
|
|
150
|
+
timeout: float = DISCOVER_TIMEOUT,
|
|
151
|
+
max_entries: int | None = None,
|
|
152
|
+
match_udn: str | None = None,
|
|
153
|
+
) -> list[UPNPEntry]:
|
|
154
|
+
"""
|
|
155
|
+
Send a message over the network to discover upnp devices.
|
|
156
|
+
|
|
157
|
+
Inspired by Crimsdings ChromeCast code
|
|
158
|
+
https://github.com/crimsdings/ [ChromeCast repository since removed]
|
|
159
|
+
"""
|
|
160
|
+
# pylint: disable=too-many-nested-blocks
|
|
161
|
+
ssdp_target = (MULTICAST_GROUP, MULTICAST_PORT)
|
|
162
|
+
|
|
163
|
+
entries: list[UPNPEntry] = []
|
|
164
|
+
|
|
165
|
+
calc_now = datetime.now
|
|
166
|
+
|
|
167
|
+
ssdp_request = build_ssdp_request(st, ssdp_mx=1)
|
|
168
|
+
sockets = []
|
|
169
|
+
try:
|
|
170
|
+
for addr in interface_addresses():
|
|
171
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
172
|
+
try:
|
|
173
|
+
sock.bind((addr, 0))
|
|
174
|
+
sock.sendto(ssdp_request, ssdp_target)
|
|
175
|
+
sockets.append(sock)
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
finally:
|
|
179
|
+
if sock not in sockets:
|
|
180
|
+
sock.close()
|
|
181
|
+
|
|
182
|
+
start = calc_now()
|
|
183
|
+
while sockets:
|
|
184
|
+
time_diff = calc_now() - start
|
|
185
|
+
|
|
186
|
+
seconds_left = max(timeout - time_diff.seconds, 0)
|
|
187
|
+
|
|
188
|
+
ready = select.select(sockets, [], [], min(1, seconds_left))[0]
|
|
189
|
+
if not ready:
|
|
190
|
+
# Only check for timeout when there are no more results. Exit
|
|
191
|
+
# if the time has expired, or probe again if there is more
|
|
192
|
+
# time remaining.
|
|
193
|
+
if seconds_left <= 0:
|
|
194
|
+
return entries
|
|
195
|
+
for sock in sockets:
|
|
196
|
+
sock.sendto(ssdp_request, ssdp_target)
|
|
197
|
+
|
|
198
|
+
for sock in ready:
|
|
199
|
+
response = sock.recv(1024).decode("UTF-8", "replace")
|
|
200
|
+
|
|
201
|
+
entry = UPNPEntry.from_response(response)
|
|
202
|
+
if entry.usn == VIRTUAL_DEVICE_USN:
|
|
203
|
+
continue # Don't return the virtual device.
|
|
204
|
+
|
|
205
|
+
# Search for devices
|
|
206
|
+
if entry not in entries:
|
|
207
|
+
if match_udn is None:
|
|
208
|
+
entries.append(entry)
|
|
209
|
+
elif match_udn == entry.udn:
|
|
210
|
+
entries.append(entry)
|
|
211
|
+
|
|
212
|
+
# Return if we've found the max number of devices
|
|
213
|
+
if max_entries and len(entries) == max_entries:
|
|
214
|
+
return entries
|
|
215
|
+
except OSError:
|
|
216
|
+
LOG.exception("Socket error while discovering SSDP devices")
|
|
217
|
+
finally:
|
|
218
|
+
for sock in sockets:
|
|
219
|
+
sock.close()
|
|
220
|
+
|
|
221
|
+
return entries
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class DiscoveryResponder:
|
|
225
|
+
"""Inform Wemo devices of the pywemo virtual Wemo device.
|
|
226
|
+
|
|
227
|
+
The DiscoveryResponder informs Wemo devices of the /setup.xml URL for the
|
|
228
|
+
pywemo virtual Wemo device. The virtual device is used for receiving long
|
|
229
|
+
press actions from Wemo devices and is integrated into the
|
|
230
|
+
SubscriptionRegistry HTTP server.
|
|
231
|
+
|
|
232
|
+
Wemo devices are informed of the pywemo virtual Wemo device in two ways:
|
|
233
|
+
|
|
234
|
+
1. Wemo devices periodically send UPnP M-SEARCH discovery requests for the
|
|
235
|
+
to locate other devices on the network. DiscoveryResponder responds to
|
|
236
|
+
these requests with the URL for the virtual device.
|
|
237
|
+
|
|
238
|
+
2. A UPnP NOTIFY message is periodically multicasted by DiscoveryResponder
|
|
239
|
+
to inform Wemo devices on the network of the URL for the virtual device.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(self, callback_port: int) -> None:
|
|
243
|
+
"""Create a server that will respond to WeMo discovery requests.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
callback_port: The port for the SubscriptionRegistry HTTP server.
|
|
247
|
+
"""
|
|
248
|
+
self.callback_port = callback_port
|
|
249
|
+
self._thread: threading.Thread | None = None
|
|
250
|
+
self._exit = threading.Event()
|
|
251
|
+
self._thread_exception: Exception | None = None
|
|
252
|
+
self._notify_enabled = True # Only ever set to False in tests.
|
|
253
|
+
self._nls_uuid = str(uuid.uuid4())
|
|
254
|
+
|
|
255
|
+
def send_notify(self, nts: str) -> None:
|
|
256
|
+
"""Send a UPnP NOTIFY message containing the virtual device URL."""
|
|
257
|
+
ssdp_target = (MULTICAST_GROUP, MULTICAST_PORT)
|
|
258
|
+
for addr in interface_addresses(): # Send on all interfaces.
|
|
259
|
+
params = {
|
|
260
|
+
"callback": get_callback_address(addr, self.callback_port),
|
|
261
|
+
"nls": self._nls_uuid,
|
|
262
|
+
"nts": nts,
|
|
263
|
+
}
|
|
264
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
265
|
+
try:
|
|
266
|
+
sock.bind((addr, 0))
|
|
267
|
+
sock.sendto(
|
|
268
|
+
(SSDP_NOTIFY % params).encode("UTF-8"), ssdp_target
|
|
269
|
+
)
|
|
270
|
+
except OSError:
|
|
271
|
+
pass
|
|
272
|
+
finally:
|
|
273
|
+
sock.close()
|
|
274
|
+
|
|
275
|
+
def respond_to_discovery(self) -> None:
|
|
276
|
+
"""Respond to a WeMo discovery request with the virtual device URL."""
|
|
277
|
+
recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
278
|
+
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
279
|
+
try:
|
|
280
|
+
recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
281
|
+
|
|
282
|
+
# Join the multicast group on all interfaces.
|
|
283
|
+
group = socket.inet_aton(MULTICAST_GROUP)
|
|
284
|
+
for addr in interface_addresses():
|
|
285
|
+
try:
|
|
286
|
+
local = socket.inet_aton(addr)
|
|
287
|
+
recv_sock.setsockopt(
|
|
288
|
+
socket.IPPROTO_IP,
|
|
289
|
+
socket.IP_ADD_MEMBERSHIP,
|
|
290
|
+
group + local,
|
|
291
|
+
)
|
|
292
|
+
except OSError as err:
|
|
293
|
+
LOG.error(
|
|
294
|
+
"Failed join multicast group on %s: %s", addr, err
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
recv_sock.bind((MULTICAST_GROUP, MULTICAST_PORT))
|
|
298
|
+
|
|
299
|
+
next_notify = datetime.min
|
|
300
|
+
while not self._exit.is_set():
|
|
301
|
+
# Periodically send NOTIFY messages.
|
|
302
|
+
now = datetime.now()
|
|
303
|
+
if now > next_notify and self._notify_enabled:
|
|
304
|
+
next_notify = now + timedelta(seconds=(MAX_AGE / 2) - 30)
|
|
305
|
+
self.send_notify("ssdp:alive")
|
|
306
|
+
|
|
307
|
+
# Check for new discovery requests.
|
|
308
|
+
if not select.select([recv_sock], [], [], 1)[0]:
|
|
309
|
+
continue # Timeout, no data. Loop again and check for exit
|
|
310
|
+
msg, sock_addr = recv_sock.recvfrom(1024)
|
|
311
|
+
lines = msg.splitlines()
|
|
312
|
+
if len(lines) < 3 or not lines[0].startswith(
|
|
313
|
+
b"M-SEARCH * HTTP"
|
|
314
|
+
):
|
|
315
|
+
continue
|
|
316
|
+
if (
|
|
317
|
+
EXPECTED_ST_HEADER not in lines
|
|
318
|
+
or EXPECTED_MAN_HEADER not in lines
|
|
319
|
+
):
|
|
320
|
+
continue
|
|
321
|
+
params = {
|
|
322
|
+
"callback": get_callback_address(
|
|
323
|
+
sock_addr[0], self.callback_port
|
|
324
|
+
),
|
|
325
|
+
"date": format_date_time(time.time()),
|
|
326
|
+
"nls": self._nls_uuid,
|
|
327
|
+
}
|
|
328
|
+
try:
|
|
329
|
+
send_sock.sendto(
|
|
330
|
+
(SSDP_REPLY % params).encode("UTF-8"), sock_addr
|
|
331
|
+
)
|
|
332
|
+
except OSError as err:
|
|
333
|
+
LOG.error(
|
|
334
|
+
"Failed to send SSDP reply to %r: %s", sock_addr, err
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if self._notify_enabled:
|
|
338
|
+
self.send_notify("ssdp:byebye")
|
|
339
|
+
except Exception as exp:
|
|
340
|
+
self._thread_exception = exp # Used in the stop() method.
|
|
341
|
+
raise
|
|
342
|
+
finally:
|
|
343
|
+
recv_sock.close()
|
|
344
|
+
send_sock.close()
|
|
345
|
+
|
|
346
|
+
def start(self) -> None:
|
|
347
|
+
"""Start the server."""
|
|
348
|
+
self._exit.clear()
|
|
349
|
+
self._thread_exception = None
|
|
350
|
+
self._thread = threading.Thread(
|
|
351
|
+
target=self.respond_to_discovery,
|
|
352
|
+
name="Wemo DiscoveryResponder Thread",
|
|
353
|
+
)
|
|
354
|
+
self._thread.start()
|
|
355
|
+
|
|
356
|
+
def stop(self) -> None:
|
|
357
|
+
"""Stop the server."""
|
|
358
|
+
if self._thread:
|
|
359
|
+
self._exit.set()
|
|
360
|
+
self._thread.join()
|
|
361
|
+
self._thread = None
|
|
362
|
+
# Improve visibility of any exceptions that occurred on the thread.
|
|
363
|
+
if self._thread_exception is not None:
|
|
364
|
+
# pylint: disable=raising-bad-type
|
|
365
|
+
raise self._thread_exception
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
if __name__ == "__main__":
|
|
369
|
+
from pprint import pprint
|
|
370
|
+
|
|
371
|
+
pprint("Scanning UPNP..")
|
|
372
|
+
pprint(scan())
|