python-mobius 0.1.1__py3-none-any.whl → 0.1.2__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
@@ -41,7 +41,7 @@ from .discovery import (
41
41
  scan_for_mobius_devices, scan_for_mobius_devices_with_info, group_by_pan_id,
42
42
  )
43
43
 
44
- __version__ = "0.1.1"
44
+ __version__ = "0.1.2"
45
45
 
46
46
  __all__ = [
47
47
  "__version__",
mobius/device.py CHANGED
@@ -131,8 +131,29 @@ class MobiusDevice:
131
131
  if attempt == 2:
132
132
  raise
133
133
  await asyncio.sleep(2.0)
134
- await self._client.start_notify(CHAR_RX_DATA, self._on_rx_data)
135
- await self._client.start_notify(CHAR_RX_FINAL, self._on_rx_final)
134
+
135
+ # IMPORTANT: from here on, self._client holds a real, already-open
136
+ # BLE connection. If anything below fails, connect() (== __aenter__)
137
+ # raises -- and per the async context manager protocol, a raising
138
+ # __aenter__ means __aexit__/disconnect() is NEVER called by the
139
+ # caller's `async with` block. Without this try/except, a failure
140
+ # here would leak an orphaned open connection on the adapter every
141
+ # single time it happens -- and since coordinators retry on a timer,
142
+ # that leak compounds over repeated failures until the adapter's
143
+ # real connection-slot pool is exhausted, independent of range or
144
+ # any other integration. Confirmed as the likely cause of "no
145
+ # available connection slot" errors appearing over time rather than
146
+ # immediately, reported against real hardware.
147
+ try:
148
+ await self._client.start_notify(CHAR_RX_DATA, self._on_rx_data)
149
+ await self._client.start_notify(CHAR_RX_FINAL, self._on_rx_final)
150
+ except Exception:
151
+ try:
152
+ if self._client.is_connected:
153
+ await self._client.disconnect()
154
+ except Exception:
155
+ pass # best-effort cleanup; the original error is what matters
156
+ raise
136
157
 
137
158
  async def disconnect(self):
138
159
  if self._client and self._client.is_connected:
@@ -345,12 +366,24 @@ class MobiusDevice:
345
366
 
346
367
  async def get_pump_telemetry(self) -> dict:
347
368
  """
348
- Returns {"speed": int, "gph": int}. Both are genuinely read live from
349
- the device -- confirmed via FlowRange.java, which is what the app's
350
- dashboard actually queries for the pump gauge widget.
369
+ Returns {"speed": int, "speed_percent": float, "gph": int}. speed
370
+ and gph are genuinely read live from the device -- confirmed via
371
+ FlowRange.java, which is what the app's dashboard actually queries
372
+ for the pump gauge widget.
351
373
 
352
374
  "gph" comes from PhysicalValues/GallonsPerHour (int32 LE).
353
- "speed" comes from MotorSpeed directly (int16).
375
+
376
+ "speed" comes from MotorSpeed directly (int16). CONFIRMED (not
377
+ inferred) to be a percentage of max pump power in tenths of a
378
+ percent, not RPM: the app's own display code does
379
+ `String.format("%.0f%%", Math.abs(currentSpeed / 10.0f))` on this
380
+ exact value (FlowFragment.java/WidgetViewCell.java). "speed_percent"
381
+ is that same abs(speed)/10 conversion, provided as a convenience --
382
+ the sign of the raw "speed" value encodes rotation direction
383
+ (reverse), matching the same convention as the schedule's MaxSpeed
384
+ parameter (see PumpPrimitiveValue), so check the sign of "speed"
385
+ yourself if direction matters; speed_percent is always non-negative.
386
+
354
387
  MotorRPM is defined in the protocol but never queried anywhere in the
355
388
  app -- not included here since it's unconfirmed whether firmware
356
389
  actually populates it.
@@ -361,7 +394,8 @@ class MobiusDevice:
361
394
  )
362
395
  speed = struct.unpack("<h", speed_raw[0])[0] if speed_raw else None
363
396
  gph = struct.unpack("<i", gph_raw[0])[0] if gph_raw else None
364
- return {"speed": speed, "gph": gph}
397
+ speed_percent = abs(speed) / 10 if speed is not None else None
398
+ return {"speed": speed, "speed_percent": speed_percent, "gph": gph}
365
399
 
366
400
  # ---- light schedule (Radion/XR15 etc.) --------------------------------
367
401
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mobius
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -24,6 +24,7 @@ Requires-Python: >=3.9
24
24
  Requires-Dist: bleak>=0.21
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: bleak-retry-connector>=3.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
28
  Requires-Dist: pytest>=7.0; extra == 'dev'
28
29
  Provides-Extra: retry
29
30
  Requires-Dist: bleak-retry-connector>=3.0; extra == 'retry'
@@ -1,14 +1,14 @@
1
- mobius/__init__.py,sha256=JK6iAhOfAJ1LBf2tKUi3OQY7NMNwOudyyExjYKbehNU,3381
1
+ mobius/__init__.py,sha256=2TkA717bDRzec96psyBxp1NEUi12azr93DxI0iYSfrY,3381
2
2
  mobius/cli.py,sha256=uhGZIDrhpTp8LkTGOl4BZVQWgJYHbaIVH2HzX3RFCCs,2642
3
3
  mobius/constants.py,sha256=wKrWU-QnqMuWuvczHwt3ha3sPw6D9NNK-kIhxJcTbHA,14702
4
4
  mobius/crc.py,sha256=WwIfUXGN_UKdsv-_Q303-L1-L1Rc1fvJnB5176ftzqU,2669
5
- mobius/device.py,sha256=VeBBT2z0XyqwdnUxw56GrG_ZlVe5Oq05tF-m4z-rMDY,20805
5
+ mobius/device.py,sha256=3kR34RLQYd3lbqG8HeRC5HMvD7k5A6XpBfu_eyASNVw,22779
6
6
  mobius/discovery.py,sha256=4MjGwpd0bUK-koiZ-a2Cvq-gCTImAKAQ4lZ-9vOge3Q,2290
7
7
  mobius/frame.py,sha256=zG2vh8PceJHD8R43MwZiJnm0Ge1dr8nFcKsJR9_c9LM,6135
8
8
  mobius/manufacturer.py,sha256=8-vBzckN8xmsPSMj5EWtOCXenbyQueoY5pnZHePgb_k,1937
9
9
  mobius/schedule.py,sha256=kHr684TIKPZ2BplIySoeD-Yn1Xa_2bE6rfGUs-waWMQ,7483
10
- python_mobius-0.1.1.dist-info/METADATA,sha256=m9dnMFZU2-Qdq_kLSpxPqoYE05Ac3E7NhtSjSmwjXNo,5907
11
- python_mobius-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
- python_mobius-0.1.1.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
13
- python_mobius-0.1.1.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
14
- python_mobius-0.1.1.dist-info/RECORD,,
10
+ python_mobius-0.1.2.dist-info/METADATA,sha256=ZfFhhqMEx8S5lyZumY2nMBlmO2bW0HrDtwFi8_gKbbE,5959
11
+ python_mobius-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
+ python_mobius-0.1.2.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
13
+ python_mobius-0.1.2.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
14
+ python_mobius-0.1.2.dist-info/RECORD,,