python-swidget 1.4.6__tar.gz → 1.4.9__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.6 → python_swidget-1.4.9}/PKG-INFO +1 -1
- {python_swidget-1.4.6 → python_swidget-1.4.9}/pyproject.toml +1 -1
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/swidgetdevice.py +30 -3
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/websocket.py +28 -23
- {python_swidget-1.4.6 → python_swidget-1.4.9}/README.md +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/__init__.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/cli.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/discovery.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/exceptions.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/provision.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/py.typed +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/swidgetdimmer.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/swidgetoutlet.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/swidgetswitch.py +0 -0
- {python_swidget-1.4.6 → python_swidget-1.4.9}/swidget/swidgettimerswitch.py +0 -0
|
@@ -361,10 +361,30 @@ class SwidgetDevice:
|
|
|
361
361
|
self.model = summary["model"]
|
|
362
362
|
self.mac_address = summary["mac"]
|
|
363
363
|
self.version = summary["version"]
|
|
364
|
-
|
|
364
|
+
new_assemblies = {
|
|
365
365
|
"host": SwidgetAssembly(summary["host"]),
|
|
366
366
|
"insert": SwidgetAssembly(summary["insert"]),
|
|
367
367
|
}
|
|
368
|
+
# Carry already-populated function state forward. Rebuilding
|
|
369
|
+
# assemblies wholesale resets every component's ``functions`` to
|
|
370
|
+
# ``None`` placeholders until the next ``state`` message lands —
|
|
371
|
+
# subscribers that read state in between (e.g. an HA coordinator
|
|
372
|
+
# firing on the summary callback) would briefly see "unknown"
|
|
373
|
+
# and flicker the UI.
|
|
374
|
+
for assembly_key, new_assembly in new_assemblies.items():
|
|
375
|
+
old_assembly = self.assemblies.get(assembly_key)
|
|
376
|
+
if old_assembly is None:
|
|
377
|
+
continue
|
|
378
|
+
for component_id, new_component in new_assembly.components.items():
|
|
379
|
+
old_component = old_assembly.components.get(component_id)
|
|
380
|
+
if old_component is None:
|
|
381
|
+
continue
|
|
382
|
+
for fn_name in new_component.functions:
|
|
383
|
+
if fn_name in old_component.functions:
|
|
384
|
+
new_component.functions[fn_name] = old_component.functions[
|
|
385
|
+
fn_name
|
|
386
|
+
]
|
|
387
|
+
self.assemblies = new_assemblies
|
|
368
388
|
self.device_type = DeviceType(self.assemblies["host"].type)
|
|
369
389
|
self.insert_type = InsertType(self.assemblies["insert"].type)
|
|
370
390
|
self.id = self.assemblies["host"].id
|
|
@@ -413,8 +433,15 @@ class SwidgetDevice:
|
|
|
413
433
|
for id, component in self.assemblies[assembly].components.items():
|
|
414
434
|
try:
|
|
415
435
|
component.functions.update(state[assembly]["components"][id])
|
|
416
|
-
except Exception:
|
|
417
|
-
|
|
436
|
+
except Exception as exc:
|
|
437
|
+
# Don't fail the whole state-process loop on one bad
|
|
438
|
+
# component, but DO surface what was skipped — silent
|
|
439
|
+
# failures here are how is_on ends up reading from a
|
|
440
|
+
# never-populated None placeholder.
|
|
441
|
+
_LOGGER.debug(
|
|
442
|
+
f"process_state: skipped {assembly}/{id} "
|
|
443
|
+
f"({type(exc).__name__}: {exc})"
|
|
444
|
+
)
|
|
418
445
|
self._last_update = int(time.time())
|
|
419
446
|
|
|
420
447
|
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
|
|
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
|
+
"""
|
|
110
116
|
_LOGGER.debug(f"[{self.host}] websocket.send_str: {message}")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
f"Failed to send message after {max_send_retries} attempts."
|
|
129
|
-
)
|
|
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."""
|
|
@@ -180,8 +184,9 @@ class SwidgetWebsocket:
|
|
|
180
184
|
)
|
|
181
185
|
|
|
182
186
|
self.retry_count += 1
|
|
183
|
-
#
|
|
184
|
-
|
|
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)
|
|
185
190
|
_LOGGER.warning(
|
|
186
191
|
f"Reconnecting to Swidget device: {self.host} in {delay} seconds (attempt {self.retry_count})..."
|
|
187
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
|