python-mobius 0.4.2__py3-none-any.whl → 0.4.3__py3-none-any.whl

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.
mobius/__init__.py CHANGED
@@ -65,7 +65,7 @@ from .discovery import (
65
65
  discover_tank,
66
66
  )
67
67
 
68
- __version__ = "0.4.2"
68
+ __version__ = "0.4.3"
69
69
 
70
70
  __all__ = [
71
71
  "__version__",
mobius/relay.py CHANGED
@@ -177,12 +177,52 @@ class RelayedMobiusDevice(MobiusDevice):
177
177
  embedded in its payload (confirmed via reverse engineering:
178
178
  coapRequest.getToken() == coapResponse.getToken()) -- never by
179
179
  this outer frame's own message ID.
180
+
181
+ Retries exactly once on a timeout before actually raising --
182
+ confirmed via reverse engineering the app's own request-timeout
183
+ handling: it never treats a single timed-out relay round-trip as
184
+ a real failure, always transparently retrying (fresh send, same
185
+ request) once first, only surfacing a failure if the retry ALSO
186
+ times out. A relayed round-trip crosses BLE (to the gateway) and
187
+ then the Thread mesh itself (gateway to target and back) -- a
188
+ single dropped/delayed packet somewhere in that longer path is
189
+ evidently common enough that the app was built assuming it will
190
+ happen regularly, not as an edge case. Each attempt gets its own
191
+ fresh CoAP token (this library generates one per call already,
192
+ not tied to a single request object the way the app's own retry
193
+ reuses one) -- deliberately, so a late, delayed response to the
194
+ first, abandoned attempt can never be mismatched against the
195
+ retry's own, separate wait.
180
196
  """
181
197
  if not self.gateway.is_connected:
182
198
  raise IOError(
183
199
  f"gateway device is not connected -- cannot relay to {self.peer.serial!r} through it"
184
200
  )
185
201
 
202
+ last_timeout_token: Optional[int] = None
203
+ for attempt in (1, 2):
204
+ try:
205
+ return await self._relay_attempt(inner_request_frame, is_retry=(attempt == 2))
206
+ except asyncio.TimeoutError as err:
207
+ last_timeout_token = err.args[0] if err.args else None
208
+ if attempt == 1 and self.debug:
209
+ print(f" [relay debug] timed out waiting {self.timeout}s for "
210
+ f"{self.peer.serial!r} -- retrying once before giving up")
211
+ continue
212
+
213
+ raise IOError(
214
+ f"timed out waiting {self.timeout}s for a relayed response from target "
215
+ f"{self.peer.serial!r} (no CoAP indication received for token {last_timeout_token}, "
216
+ f"even after one retry)"
217
+ )
218
+
219
+ async def _relay_attempt(self, inner_request_frame: bytes, is_retry: bool) -> ParsedFrame:
220
+ """One full send-and-await cycle for _relay() -- split out so
221
+ _relay() itself can retry it once on a timeout without
222
+ duplicating this whole body. Raises asyncio.TimeoutError itself
223
+ (not wrapped in IOError) specifically so _relay() can catch just
224
+ that case to decide whether to retry; every other exception here
225
+ propagates as-is, unwrapped, straight out of _relay() too."""
186
226
  token = next_coap_token()
187
227
  # Confirmed always GET regardless of the inner operation -- see
188
228
  # this module's docstring for why.
@@ -193,7 +233,8 @@ class RelayedMobiusDevice(MobiusDevice):
193
233
  outer_frame = build_frame(OPGROUP_C2CI_REQUEST, COAP_OPCODE, coap_payload, outer_message_id)
194
234
 
195
235
  if self.debug:
196
- print(f" [relay debug] inner request frame ({len(inner_request_frame)}B): "
236
+ retry_note = " (retry)" if is_retry else ""
237
+ print(f" [relay debug] inner request frame{retry_note} ({len(inner_request_frame)}B): "
197
238
  f"{inner_request_frame.hex()}")
198
239
  print(f" [relay debug] target address: {self.peer.address.hex()} token: {token}")
199
240
  print(f" [relay debug] outer request frame ({len(outer_frame)}B): {outer_frame.hex()}")
@@ -220,10 +261,7 @@ class RelayedMobiusDevice(MobiusDevice):
220
261
  coap_response = await asyncio.wait_for(coap_future, timeout=self.timeout)
221
262
  except asyncio.TimeoutError:
222
263
  self.gateway._pending_coap_tokens.pop(token, None)
223
- raise IOError(
224
- f"timed out waiting {self.timeout}s for a relayed response from target "
225
- f"{self.peer.serial!r} (no CoAP indication received for token {token})"
226
- )
264
+ raise asyncio.TimeoutError(token) from None
227
265
  finally:
228
266
  if self.debug:
229
267
  self.gateway.on_any_frame = previous_hook
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-mobius
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)
5
5
  Project-URL: Homepage, https://code.r3pek.org/r3pek/python-mobius
6
6
  Project-URL: Documentation, https://code.r3pek.org/r3pek/python-mobius/src/branch/main/documentation
@@ -1,4 +1,4 @@
1
- mobius/__init__.py,sha256=BpkSMm-tv7BC2F_vWHd-jym24Hsmwfd_OSMeJa2FEfQ,5722
1
+ mobius/__init__.py,sha256=7MOkrkQcDD7HLG5C7hKh5BTzUp0PmB5ZfIa3esYn2Nc,5722
2
2
  mobius/cli.py,sha256=8XH-pVTX4KObzd0IOGXicT7KrcNEjm5xYZS8JOWfHDA,30726
3
3
  mobius/coap.py,sha256=eXf_-9heQD1UjLyqZU9KA-c0jB50NGzAd8D7d6kFMb0,10750
4
4
  mobius/constants.py,sha256=z_KrFev5vGD9-9lKn4J0nGVtzTmelGQ13bYlVcCdjm0,25217
@@ -12,10 +12,10 @@ mobius/mesh_address.py,sha256=PKQ-mm7Z4bY11ZnVmeWZO9EmOiAHp8hNtXceMGMs2tk,5940
12
12
  mobius/modifiers.py,sha256=zWJCsjaJ4TtwS1QgQoRFH-ve1mykuPWn5KX4YQpNqco,7215
13
13
  mobius/power.py,sha256=5l9ehjNCDrSsJjQkaU4M6pYnSMqw2FVF8Ogg6KIE5vQ,3844
14
14
  mobius/pump_status.py,sha256=RWTl8ZViCFDbiFU2mX-WAIH_cWhZOZ0M2B0oYXnPx3Y,1598
15
- mobius/relay.py,sha256=tMpV_67CYt9A1L_DPERzbqOgy3BOdG4NZpAG_IlefhY,16587
15
+ mobius/relay.py,sha256=bo2uUV6HhHD_-LuH4CkuPJtnKGQGM6crmB6kLAU8i4E,18857
16
16
  mobius/schedule.py,sha256=d5iAHvKuFDwfbrSptPx6viiA_lj2qGfWhsZwM_Bpr1k,10974
17
- python_mobius-0.4.2.dist-info/METADATA,sha256=PBqNDTyqaF-wG4Q8YUZsMqsQ2TkBTFfOoUNKe0yYZG4,5952
18
- python_mobius-0.4.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
19
- python_mobius-0.4.2.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
20
- python_mobius-0.4.2.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
21
- python_mobius-0.4.2.dist-info/RECORD,,
17
+ python_mobius-0.4.3.dist-info/METADATA,sha256=Iht60XXLlvjteO-wjyLMwUlzeKGlDKRL-mgIlv-sHx8,5952
18
+ python_mobius-0.4.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
19
+ python_mobius-0.4.3.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
20
+ python_mobius-0.4.3.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
21
+ python_mobius-0.4.3.dist-info/RECORD,,