python-swidget 1.4.5__tar.gz → 1.4.8__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.5 → python_swidget-1.4.8}/PKG-INFO +1 -1
- {python_swidget-1.4.5 → python_swidget-1.4.8}/pyproject.toml +1 -1
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/swidgetdevice.py +21 -4
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/websocket.py +43 -27
- {python_swidget-1.4.5 → python_swidget-1.4.8}/README.md +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/__init__.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/cli.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/discovery.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/exceptions.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/provision.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/py.typed +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/swidgetdimmer.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/swidgetswitch.py +0 -0
- {python_swidget-1.4.5 → python_swidget-1.4.8}/swidget/swidgettimerswitch.py +0 -0
|
@@ -249,7 +249,9 @@ class SwidgetDevice:
|
|
|
249
249
|
raise ValueError(f"Unsupported HTTP method: {http_method}")
|
|
250
250
|
|
|
251
251
|
url = f"{self.uri_scheme}://{self.ip_address}/api/v1/{endpoint}"
|
|
252
|
-
_LOGGER.debug(
|
|
252
|
+
_LOGGER.debug(
|
|
253
|
+
f"HTTP {http_method} {url} params={params} body={json_payload}"
|
|
254
|
+
)
|
|
253
255
|
|
|
254
256
|
try:
|
|
255
257
|
async with self._session.request(
|
|
@@ -261,14 +263,22 @@ class SwidgetDevice:
|
|
|
261
263
|
) as response:
|
|
262
264
|
if response.status == 200:
|
|
263
265
|
if response.content_length == 0:
|
|
266
|
+
_LOGGER.debug(
|
|
267
|
+
f"HTTP {response.status} {url} (empty body)"
|
|
268
|
+
)
|
|
264
269
|
return {}
|
|
265
|
-
|
|
270
|
+
body = await response.json()
|
|
271
|
+
_LOGGER.debug(f"HTTP {response.status} {url} body={body}")
|
|
272
|
+
return body
|
|
266
273
|
elif response.status == 403:
|
|
267
274
|
_LOGGER.error(
|
|
268
275
|
f"Authentication failed for {http_method} '{endpoint}'"
|
|
269
276
|
)
|
|
270
277
|
raise SwidgetAuthenticationException
|
|
271
278
|
else:
|
|
279
|
+
_LOGGER.debug(
|
|
280
|
+
f"HTTP {response.status} {url} (non-success)"
|
|
281
|
+
)
|
|
272
282
|
response.raise_for_status()
|
|
273
283
|
return {}
|
|
274
284
|
except ClientConnectorError as e:
|
|
@@ -403,8 +413,15 @@ class SwidgetDevice:
|
|
|
403
413
|
for id, component in self.assemblies[assembly].components.items():
|
|
404
414
|
try:
|
|
405
415
|
component.functions.update(state[assembly]["components"][id])
|
|
406
|
-
except Exception:
|
|
407
|
-
|
|
416
|
+
except Exception as exc:
|
|
417
|
+
# Don't fail the whole state-process loop on one bad
|
|
418
|
+
# component, but DO surface what was skipped — silent
|
|
419
|
+
# failures here are how is_on ends up reading from a
|
|
420
|
+
# never-populated None placeholder.
|
|
421
|
+
_LOGGER.debug(
|
|
422
|
+
f"process_state: skipped {assembly}/{id} "
|
|
423
|
+
f"({type(exc).__name__}: {exc})"
|
|
424
|
+
)
|
|
408
425
|
self._last_update = int(time.time())
|
|
409
426
|
|
|
410
427
|
async def update(self) -> None:
|
|
@@ -27,7 +27,7 @@ class SwidgetWebsocket:
|
|
|
27
27
|
session: aiohttp.ClientSession | None = None,
|
|
28
28
|
use_security: bool = True,
|
|
29
29
|
verify_ssl: bool = False,
|
|
30
|
-
retry_interval: int =
|
|
30
|
+
retry_interval: int = 5, # Initial retry interval in seconds
|
|
31
31
|
max_retries: int | None = None, # Maximum number of reconnection attempts
|
|
32
32
|
):
|
|
33
33
|
"""Initialize the SwidgetWebsocket.
|
|
@@ -106,27 +106,31 @@ class SwidgetWebsocket:
|
|
|
106
106
|
self._client = None
|
|
107
107
|
|
|
108
108
|
async def send_str(self, message: str) -> None:
|
|
109
|
-
"""Send a string message through the websocket
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
109
|
+
"""Send a string message through the websocket.
|
|
110
|
+
|
|
111
|
+
Drops the message (no retry) if the connection is missing or in
|
|
112
|
+
a closing state. Retrying against a closing transport just wastes
|
|
113
|
+
time — the right recovery is to invalidate the client so the
|
|
114
|
+
run() loop's reconnect kicks in on the next iteration.
|
|
115
|
+
"""
|
|
116
|
+
_LOGGER.debug(f"[{self.host}] websocket.send_str: {message}")
|
|
117
|
+
if self._client is None or self._client.closed:
|
|
118
|
+
if self._client is not None:
|
|
119
|
+
# aiohttp says it's closed but we still hold a reference;
|
|
120
|
+
# clear it so run() reconnects on the next loop.
|
|
121
|
+
self._client = None
|
|
122
|
+
_LOGGER.warning(
|
|
123
|
+
f"[{self.host}] websocket not connected, dropping message"
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
await self._client.send_str(message)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
_LOGGER.warning(
|
|
130
|
+
f"[{self.host}] websocket send failed ({e}); invalidating client"
|
|
131
|
+
)
|
|
132
|
+
# Force the run() loop's reconnect path on the next iteration.
|
|
133
|
+
self._client = None
|
|
130
134
|
|
|
131
135
|
async def receive(self) -> Any | None:
|
|
132
136
|
"""Receive a message from the WebSocket server."""
|
|
@@ -139,13 +143,24 @@ class SwidgetWebsocket:
|
|
|
139
143
|
_LOGGER.debug(f"[{self.host}] Received message: {message_data}")
|
|
140
144
|
return message_data
|
|
141
145
|
elif message.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
|
|
142
|
-
|
|
146
|
+
# message.data carries the close code (int) when present;
|
|
147
|
+
# message.extra carries the reason string. close_code on
|
|
148
|
+
# the client also reflects the final negotiated code.
|
|
149
|
+
_LOGGER.error(
|
|
150
|
+
f"[{self.host}] Websocket closed "
|
|
151
|
+
f"(type={message.type.name}, "
|
|
152
|
+
f"code={message.data!r}, "
|
|
153
|
+
f"reason={message.extra!r}, "
|
|
154
|
+
f"client.close_code={getattr(self._client, 'close_code', None)!r})"
|
|
155
|
+
)
|
|
143
156
|
self._client = None
|
|
144
157
|
elif message.type == WSMsgType.ERROR:
|
|
145
|
-
_LOGGER.error(
|
|
158
|
+
_LOGGER.error(
|
|
159
|
+
f"[{self.host}] Websocket error: {message.data!r}"
|
|
160
|
+
)
|
|
146
161
|
self._client = None
|
|
147
162
|
except Exception as e:
|
|
148
|
-
_LOGGER.error(f"Error receiving message: {e}")
|
|
163
|
+
_LOGGER.error(f"Error receiving message: {e}", exc_info=True)
|
|
149
164
|
return None
|
|
150
165
|
|
|
151
166
|
async def close(self) -> None:
|
|
@@ -169,8 +184,9 @@ class SwidgetWebsocket:
|
|
|
169
184
|
)
|
|
170
185
|
|
|
171
186
|
self.retry_count += 1
|
|
172
|
-
#
|
|
173
|
-
|
|
187
|
+
# Exponential backoff capped at 60s so a long outage doesn't push
|
|
188
|
+
# the next attempt out by hours.
|
|
189
|
+
delay = min(self.retry_interval * (2 ** (self.retry_count - 1)), 60)
|
|
174
190
|
_LOGGER.warning(
|
|
175
191
|
f"Reconnecting to Swidget device: {self.host} in {delay} seconds (attempt {self.retry_count})..."
|
|
176
192
|
)
|
|
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
|