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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-swidget
3
- Version: 1.4.5
3
+ Version: 1.4.8
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.5"
3
+ version = "1.4.8"
4
4
  description = "Python API for Swidget smart devices"
5
5
  license = "GPL-3.0-or-later"
6
6
  authors = ["Swidget"]
@@ -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(f"Sending {http_method} request to: {url}")
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
- return await response.json()
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
- pass
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, # Initial retry interval in seconds
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 with retry attempts."""
110
- _LOGGER.debug("websocket.send_str() called")
111
- max_send_retries = 3
112
- for attempt in range(max_send_retries):
113
- try:
114
- if self._client is not None:
115
- await self._client.send_str(message)
116
- return
117
- else:
118
- _LOGGER.warning("Websocket is not connected, not sending")
119
- return
120
- except Exception as e:
121
- _LOGGER.warning(
122
- f"Error sending message, attempt {attempt}/{max_send_retries}: {e}"
123
- )
124
- if attempt < max_send_retries - 1:
125
- await asyncio.sleep(5**attempt)
126
- else:
127
- _LOGGER.error(
128
- f"Failed to send message after {max_send_retries} attempts."
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
- _LOGGER.error("Websocket connection is closed")
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("WebSocket 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
- # Implement exponential backoff for reconnection delay
173
- delay = self.retry_interval * (2 ** (self.retry_count - 1))
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