python-swidget 1.4.12__tar.gz → 1.4.14__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-swidget
3
- Version: 1.4.12
3
+ Version: 1.4.14
4
4
  Summary: Python API for Swidget smart devices
5
5
  Home-page: https://github.com/swidget/python-swidget
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-swidget"
3
- version = "1.4.12"
3
+ version = "1.4.14"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -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
- mac_address = headers["USN"].split("-")[-1]
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
- if headers["ST"] in SWIDGET_STS:
95
- device_type = headers["SERVER"].split(" ")[1].split("+")[0]
96
- insert_type = headers["SERVER"].split(" ")[1].split("+")[1].split("/")[0]
97
- friendly_name = headers["SERVER"].split("/")[2].strip('"')
98
- devices[mac_address] = SwidgetDiscoveredDevice(
99
- mac=mac_address,
100
- host=ip_address,
101
- friendly_name=friendly_name,
102
- host_type=device_type,
103
- insert_type=insert_type,
104
- )
105
- _LOGGER.debug(
106
- f"Discovered Swidget device via SSDP: '{friendly_name}' at {ip_address} Type:{device_type}/{insert_type}"
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):
@@ -924,6 +924,26 @@ class SwidgetDevice:
924
924
  {"insert": {"components": {"video": {"rtsp": {"enable": bool(enabled)}}}}}
925
925
  )
926
926
 
927
+ @property
928
+ def rtsp_stream_source(self) -> Optional[str]:
929
+ """Return the device's RTSP URL, or None when unavailable.
930
+
931
+ Path ``/ph264`` is fixed by firmware; the port comes from device
932
+ config. None if RTSP is disabled, the insert isn't video, or the
933
+ config hasn't been loaded yet.
934
+ """
935
+ if self.rtsp_enabled is not True:
936
+ return None
937
+ try:
938
+ port = int(
939
+ self.device_config.config["insert"]["components"]["video"]["rtsp"][
940
+ "port"
941
+ ]
942
+ )
943
+ except (KeyError, TypeError, ValueError):
944
+ return None
945
+ return f"rtsp://{self.ip_address}:{port}/ph264"
946
+
927
947
  async def __aenter__(self) -> "SwidgetDevice":
928
948
  """Initialize and connect the Swidget Websocket client."""
929
949
  await self.connect()