python-swidget 1.4.13__tar.gz → 1.4.15__tar.gz
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.
- {python_swidget-1.4.13 → python_swidget-1.4.15}/PKG-INFO +1 -1
- {python_swidget-1.4.13 → python_swidget-1.4.15}/pyproject.toml +1 -1
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/discovery.py +50 -15
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/swidgetdevice.py +34 -6
- {python_swidget-1.4.13 → python_swidget-1.4.15}/README.md +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/__init__.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/cli.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/exceptions.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/provision.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/py.typed +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/swidgetdimmer.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/swidgetswitch.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/swidgettimerswitch.py +0 -0
- {python_swidget-1.4.13 → python_swidget-1.4.15}/swidget/websocket.py +0 -0
|
@@ -18,6 +18,36 @@ from .swidgettimerswitch import SwidgetTimerSwitch
|
|
|
18
18
|
|
|
19
19
|
RESPONSE_SEC = 5
|
|
20
20
|
SWIDGET_STS = ("urn:swidget:pico:1", "urn:swidget:video:1")
|
|
21
|
+
|
|
22
|
+
# Per-ST raw device-id length (in hex chars) before any UUID padding. The
|
|
23
|
+
# firmware embeds the device id in the SSDP USN differently per family:
|
|
24
|
+
# pico uses a fixed UUID prefix + the 12-char id as the last segment;
|
|
25
|
+
# video pads the 24-char id with zeros to fit a 32-char UUID. Both can
|
|
26
|
+
# be recovered as long as we know the original length per ST.
|
|
27
|
+
_USN_DEVICE_ID_LENGTH = {
|
|
28
|
+
"urn:swidget:pico:1": 12,
|
|
29
|
+
"urn:swidget:video:1": 24,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def device_id_from_ssdp(usn: str, st: str) -> Optional[str]:
|
|
34
|
+
"""Extract the canonical device id from an SSDP USN/ST pair.
|
|
35
|
+
|
|
36
|
+
Returns None when the inputs aren't recognized — callers should treat
|
|
37
|
+
that as "skip this discovery" rather than synthesizing an id.
|
|
38
|
+
"""
|
|
39
|
+
if not usn or not usn.startswith("uuid:") or st not in _USN_DEVICE_ID_LENGTH:
|
|
40
|
+
return None
|
|
41
|
+
uuid_part = usn[len("uuid:") :]
|
|
42
|
+
expected_len = _USN_DEVICE_ID_LENGTH[st]
|
|
43
|
+
if st == "urn:swidget:pico:1":
|
|
44
|
+
# Last hyphenated segment carries the real 12-char MAC.
|
|
45
|
+
last = uuid_part.rsplit("-", 1)[-1]
|
|
46
|
+
return last if len(last) == expected_len else None
|
|
47
|
+
# Video (and any future variant that pads): dehyphenate, take the
|
|
48
|
+
# leading expected_len chars, ignore the trailing zero padding.
|
|
49
|
+
flat = uuid_part.replace("-", "")
|
|
50
|
+
return flat[:expected_len] if len(flat) >= expected_len else None
|
|
21
51
|
# Generous timeout: TLS handshakes on the device's MCU can take several
|
|
22
52
|
# seconds on first connection.
|
|
23
53
|
DETECT_TIMEOUT_SEC = 10
|
|
@@ -89,22 +119,27 @@ class SwidgetProtocol(ssdp.SimpleServiceDiscoveryProtocol):
|
|
|
89
119
|
def response_received(self, response: ssdp.SSDPResponse, addr: tuple):
|
|
90
120
|
"""Handle an incoming response."""
|
|
91
121
|
headers = {h[0]: h[1] for h in response.headers}
|
|
92
|
-
|
|
122
|
+
st = headers.get("ST", "")
|
|
123
|
+
if st not in SWIDGET_STS:
|
|
124
|
+
return
|
|
125
|
+
device_id = device_id_from_ssdp(headers.get("USN", ""), st)
|
|
126
|
+
if not device_id:
|
|
127
|
+
_LOGGER.debug("Skipping SSDP response with unparseable USN: %s", headers)
|
|
128
|
+
return
|
|
93
129
|
ip_address = urlparse(headers["LOCATION"]).hostname
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
130
|
+
device_type = headers["SERVER"].split(" ")[1].split("+")[0]
|
|
131
|
+
insert_type = headers["SERVER"].split(" ")[1].split("+")[1].split("/")[0]
|
|
132
|
+
friendly_name = headers["SERVER"].split("/")[2].strip('"')
|
|
133
|
+
devices[device_id] = SwidgetDiscoveredDevice(
|
|
134
|
+
mac=device_id,
|
|
135
|
+
host=ip_address,
|
|
136
|
+
friendly_name=friendly_name,
|
|
137
|
+
host_type=device_type,
|
|
138
|
+
insert_type=insert_type,
|
|
139
|
+
)
|
|
140
|
+
_LOGGER.debug(
|
|
141
|
+
f"Discovered Swidget device via SSDP: '{friendly_name}' at {ip_address} Type:{device_type}/{insert_type}"
|
|
142
|
+
)
|
|
108
143
|
|
|
109
144
|
|
|
110
145
|
async def discover_devices(timeout=RESPONSE_SEC):
|
|
@@ -249,6 +249,9 @@ class SwidgetDevice:
|
|
|
249
249
|
):
|
|
250
250
|
_LOGGER.debug("Calling SwidgetDevice.process_state()")
|
|
251
251
|
await self.process_state(message)
|
|
252
|
+
elif message["request_id"] == "device_config":
|
|
253
|
+
_LOGGER.debug("Calling SwidgetDevice.process_device_config()")
|
|
254
|
+
await self.process_device_config(message)
|
|
252
255
|
else:
|
|
253
256
|
message_type = ["request_id"]
|
|
254
257
|
_LOGGER.error(
|
|
@@ -374,11 +377,33 @@ class SwidgetDevice:
|
|
|
374
377
|
raise SwidgetConnectionException from e
|
|
375
378
|
|
|
376
379
|
async def get_device_config(self) -> Any:
|
|
377
|
-
"""
|
|
380
|
+
"""Refresh the local device_config cache.
|
|
381
|
+
|
|
382
|
+
Uses the websocket when available — the response lands on
|
|
383
|
+
``message_callback`` with ``request_id == "device_config"`` and
|
|
384
|
+
``process_device_config`` updates the cache. Falls back to HTTP
|
|
385
|
+
before the socket is connected (e.g. during entry pre-load).
|
|
386
|
+
"""
|
|
378
387
|
_LOGGER.debug("SwidgetDevice.get_device_config() called")
|
|
379
|
-
|
|
388
|
+
if self.use_websockets and self.connected:
|
|
389
|
+
_LOGGER.debug("In websocket mode. Sending get_device_config over websocket")
|
|
390
|
+
await self._websocket.send_str(
|
|
391
|
+
json.dumps(
|
|
392
|
+
{"type": "get_device_config", "request_id": "device_config"}
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
_LOGGER.debug("In http mode. Sending get_device_config over http")
|
|
380
397
|
config = await self.make_http_request("GET", "device_config")
|
|
381
|
-
self.
|
|
398
|
+
await self.process_device_config(config)
|
|
399
|
+
|
|
400
|
+
async def process_device_config(self, config) -> None:
|
|
401
|
+
"""Process a device_config payload from HTTP body or websocket message."""
|
|
402
|
+
_LOGGER.debug("SwidgetDevice.process_device_config() called")
|
|
403
|
+
# Strip transport metadata so DeviceConfiguration sees the same
|
|
404
|
+
# shape regardless of whether the payload arrived via HTTP or WS.
|
|
405
|
+
cfg = {k: v for k, v in config.items() if k != "request_id"}
|
|
406
|
+
self.device_config = DeviceConfiguration(cfg)
|
|
382
407
|
self._last_update = int(time.time())
|
|
383
408
|
|
|
384
409
|
async def set_device_config(self, updates: Dict[str, Any]) -> None:
|
|
@@ -499,7 +524,12 @@ class SwidgetDevice:
|
|
|
499
524
|
self._last_update = int(time.time())
|
|
500
525
|
|
|
501
526
|
async def update(self) -> None:
|
|
502
|
-
"""
|
|
527
|
+
"""Refresh state and summary; device_config is fetched separately.
|
|
528
|
+
|
|
529
|
+
device_config rarely changes, and a successful set_device_config
|
|
530
|
+
already pushes a fresh copy into the local cache, so we don't
|
|
531
|
+
re-fetch it on every coordinator poll.
|
|
532
|
+
"""
|
|
503
533
|
_LOGGER.debug("SwidgetDevice.update() called")
|
|
504
534
|
if self._last_update == 0:
|
|
505
535
|
_LOGGER.debug("Performing the initial update to obtain sysinfo")
|
|
@@ -507,8 +537,6 @@ class SwidgetDevice:
|
|
|
507
537
|
await self.get_state()
|
|
508
538
|
if self._friendly_name == "Unknown Swidget Device":
|
|
509
539
|
await self.get_friendly_name()
|
|
510
|
-
if not self.device_config.config_populated():
|
|
511
|
-
await self.get_device_config()
|
|
512
540
|
elif (int(time.time()) - self._last_update) < 5:
|
|
513
541
|
_LOGGER.debug("update() recently called, not executing")
|
|
514
542
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|