python-mobius 0.4.3__tar.gz → 0.5.0__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.
Files changed (71) hide show
  1. {python_mobius-0.4.3 → python_mobius-0.5.0}/CHANGELOG.md +52 -3
  2. {python_mobius-0.4.3 → python_mobius-0.5.0}/PKG-INFO +37 -8
  3. {python_mobius-0.4.3 → python_mobius-0.5.0}/README.md +36 -7
  4. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/03-attributes-and-opcodes.md +6 -2
  5. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/04-device-identity.md +1 -1
  6. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/05-scenes.md +2 -2
  7. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/06-light-schedule.md +41 -13
  8. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/07-pump-schedule.md +7 -7
  9. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/08-manufacturer-data.md +38 -13
  10. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/09-thread-coap-relay.md +77 -0
  11. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/10-known-gaps-and-open-questions.md +36 -0
  12. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/11-device-status-attributes.md +103 -2
  13. {python_mobius-0.4.3 → python_mobius-0.5.0}/pyproject.toml +1 -1
  14. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/__init__.py +5 -5
  15. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/cli.py +98 -6
  16. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/constants.py +311 -91
  17. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/device.py +492 -61
  18. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/device_status.py +146 -6
  19. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/discovery.py +11 -4
  20. python_mobius-0.5.0/src/mobius/manufacturer.py +72 -0
  21. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/modifiers.py +6 -6
  22. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/relay.py +37 -2
  23. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/schedule.py +5 -6
  24. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_connection_cleanup.py +61 -2
  25. python_mobius-0.5.0/tests/test_device_control.py +59 -0
  26. python_mobius-0.5.0/tests/test_device_status.py +373 -0
  27. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_device_summary.py +47 -2
  28. python_mobius-0.5.0/tests/test_device_time.py +241 -0
  29. python_mobius-0.5.0/tests/test_discovery_company_ids.py +107 -0
  30. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_firmware_versions.py +82 -6
  31. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_light_intensity_branching.py +125 -0
  32. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_light_schedule.py +1 -3
  33. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_manufacturer.py +26 -0
  34. python_mobius-0.5.0/tests/test_manufacturer_lookup.py +57 -0
  35. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_modifiers.py +1 -2
  36. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_networked_thread_devices.py +13 -16
  37. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_pump_schedule.py +5 -5
  38. python_mobius-0.5.0/tests/test_pump_telemetry.py +133 -0
  39. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_relay.py +132 -1
  40. python_mobius-0.4.3/src/mobius/manufacturer.py +0 -53
  41. python_mobius-0.4.3/tests/test_device_status.py +0 -167
  42. python_mobius-0.4.3/tests/test_device_time.py +0 -125
  43. python_mobius-0.4.3/tests/test_manufacturer_lookup.py +0 -26
  44. {python_mobius-0.4.3 → python_mobius-0.5.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
  45. {python_mobius-0.4.3 → python_mobius-0.5.0}/.forgejo/workflows/release.yml +0 -0
  46. {python_mobius-0.4.3 → python_mobius-0.5.0}/.gitignore +0 -0
  47. {python_mobius-0.4.3 → python_mobius-0.5.0}/LICENSE +0 -0
  48. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/00-overview.md +0 -0
  49. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/01-ble-transport.md +0 -0
  50. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/02-framing-and-crc.md +0 -0
  51. {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/12-device-identity-and-address-stability.md +0 -0
  52. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/coap.py +0 -0
  53. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/crc.py +0 -0
  54. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/frame.py +0 -0
  55. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/mesh_address.py +0 -0
  56. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/power.py +0 -0
  57. {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/pump_status.py +0 -0
  58. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_coap.py +0 -0
  59. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_coap_indication_handling.py +0 -0
  60. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_frame.py +0 -0
  61. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_frame_sniffer.py +0 -0
  62. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_hyperdrive_device.py +0 -0
  63. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_insolation_detection.py +0 -0
  64. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_address.py +0 -0
  65. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_discovery.py +0 -0
  66. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
  67. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_power.py +0 -0
  68. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_pump_status.py +0 -0
  69. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_schedule_intensity_scalar.py +0 -0
  70. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_serial_decoding.py +0 -0
  71. {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_serial_identity.py +0 -0
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - get_pump_telemetry() now returns gph_reliable/minimum_gph/maximum_gph
6
+ -- confirmed via the app's own logic that a raw gph reading isn't
7
+ trustworthy without a supported flow range (a real AI Axis 20
8
+ reported ~8x its own rated max flow).
9
+ - Added support for AquaIllumination devices' own BLE company ID
10
+ (0x0001, alongside EcoTech Marine's 0x0202) -- confirmed via a real
11
+ AI Axis 20 capture. BREAKING: MOBIUS_COMPANY_ID renamed to
12
+ MOBIUS_COMPANY_ID_ECOTECH.
13
+ - Added get_lunar_phase_info(): the device's own firmware-computed
14
+ lunar state, confirmed against real hardware, wired into
15
+ --dump-schedule for comparing against the calculated lunar value.
16
+ - Fixed get_lunar_phase_info() silently returning None on a real read
17
+ failure (indistinguishable from "unsupported") -- now only an empty
18
+ response does that; an actual failure propagates.
19
+ - Documented a confirmed bug in the app itself (double timezone
20
+ application) that can make its own displayed lunar value roll over
21
+ to the next day early -- this library's own calculation is correct
22
+ and unaffected.
23
+ - Added get_advanced_features(): VorTech's "Local Control"/"Led Auto
24
+ Dim" and Radion's "Max Fan Speed"/"Fan Shutdown".
25
+ - Added motor_power_watts to get_pump_telemetry() (Vectra/NYOS Quantum).
26
+ - Added get_vectra_info() and get_coffee_info() (Vectra/NYOS Quantum
27
+ settings, no real hardware to verify against).
28
+ - Fixed get_firmware_versions() showing raw internal names instead of
29
+ proper labels for AquaIllumination devices (Orbit/Axis, Prime/Hydra).
30
+ - RelayedMobiusDevice now fails fast on later reads in the same poll
31
+ cycle after one relay timeout, instead of each one separately
32
+ waiting out its own timeout.
33
+ - Added set_time_to_now() -- writes Epoch with group=1, confirmed
34
+ against real hardware to propagate to the rest of a tank's mesh.
35
+ - Added reboot() -- soft-reboots a device (Reset attribute), matching
36
+ the app's own "Restart" button. Confirmed against real hardware
37
+ directly connected.
38
+
39
+ ## 0.4.4
40
+
41
+ - Filled in the rest of the device model list (EcoTech Marine,
42
+ AquaIllumination, Neptune Systems, NYOS), previously a curated
43
+ subset. Also corrects Model.VersaVX1, mismarked as AquaIllumination;
44
+ it's an EcoTech Marine product.
45
+ - Several Model entries are now named for their real product (e.g.
46
+ Model.Axis40, Model.Blade, Model.Quantum160) instead of the app's
47
+ own internal placeholder codename (Turtle2, Saddle, Coffee1).
48
+ - Fixed the lunar-phase-scaled light intensity sometimes disagreeing
49
+ with the app for hours at a stretch. The lunar calculation now uses
50
+ the gateway device's own reported date instead of the local
51
+ system's, matching what the app itself actually does.
52
+
3
53
  ## 0.4.3
4
54
 
5
55
  - Fixed relayed devices (anything reached through a gateway over the
@@ -230,9 +280,8 @@
230
280
 
231
281
  - **`get_hardware_info()` now decodes `Color`/`ProductType`/`RadioType`/
232
282
  `MotorType` into confirmed display labels**, instead of returning raw
233
- bytes. Each is itself a confirmed enum with confirmed labels
234
- (`M.Color`/`M.ProductType`/`M.RadioType`/`M.MotorType` in the
235
- decompile) -- added `Color`, `RadioType`, `MotorType`, `ProductType`
283
+ bytes. Each is itself a confirmed enum with confirmed labels --
284
+ added `Color`, `RadioType`, `MotorType`, `ProductType`
236
285
  enums plus their `*_LABELS` dicts to `mobius.constants`. An
237
286
  unrecognized value falls back to `"Unknown (N)"` rather than raising.
238
287
  `Revision`/`Segments` still have no confirmed enum meaning -- returned
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-mobius
3
- Version: 0.4.3
3
+ Version: 0.5.0
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
@@ -49,12 +49,18 @@ inferred/experimental.
49
49
  ## Status
50
50
 
51
51
  Alpha. Core protocol (framing, CRC, attribute get/set, scenes), pump
52
- telemetry, pump schedules, light schedules, and device discovery/grouping
53
- are implemented and verified against real hardware (two VorTech MP40QD
54
- pumps, two Radion XR15 G6 Pro lights). See
52
+ telemetry, pump schedules, light schedules, device discovery/grouping
53
+ (both EcoTech Marine's and AquaIllumination's own BLE company IDs), and
54
+ Thread/CoAP relay (reading a non-gateway tank member through the
55
+ gateway's own connection) are implemented and verified against real
56
+ hardware (two VorTech MP40QD pumps, two Radion XR15 G6 Pro lights, one
57
+ AquaIllumination Axis 20 pump). Two write operations are also confirmed
58
+ against real hardware: rebooting a device, and syncing a device's own
59
+ clock to the current time. See
55
60
  [`documentation/10-known-gaps-and-open-questions.md`](./documentation/10-known-gaps-and-open-questions.md)
56
- for what isn't covered yet (dosers, environmental sensors, Thread/CoAP
57
- device relay).
61
+ for what isn't covered yet (dosers, environmental sensors) or is
62
+ implemented but not yet verified against real hardware (Vectra/NYOS
63
+ Quantum-specific settings).
58
64
 
59
65
  ## Install
60
66
 
@@ -96,14 +102,28 @@ mobius-scan --adapter hci0
96
102
  - **Discover devices** and group them by tank/mesh (`pan_id`), reading
97
103
  model/serial straight from BLE advertisements — no connection required.
98
104
  - **Read pump telemetry**: current speed, estimated flow (GPH), operation
99
- state, error state.
105
+ state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts.
100
106
  - **Read pump schedules**: which mode (constant speed, tidal swell, pulse,
101
107
  etc.) is active at any given time, exactly as programmed.
102
108
  - **Read light schedules**: per-channel intensity at any given time,
103
109
  replicating the app's own client-side interpolation (there's no "current
104
110
  intensity" attribute — lights only expose the programmed curve).
111
+ - **Read device-specific settings**: VorTech's own "Local Control"/"Led
112
+ Auto Dim" and Radion's own "Max Fan Speed"/"Fan Shutdown"
113
+ (`get_advanced_features()`), plus Vectra and NYOS Quantum settings
114
+ (`get_vectra_info()`/`get_coffee_info()` — no real hardware to verify
115
+ either against, see
116
+ [known gaps](./documentation/10-known-gaps-and-open-questions.md)).
105
117
  - **Control scenes**: start feed mode, resume the normal schedule, or any
106
118
  other configured scene.
119
+ - **Fix a desynced device clock** (`set_time_to_now()`) — a WRITE.
120
+ Writing to one device appears to propagate to the rest of its Thread
121
+ mesh too, confirmed against real hardware — see
122
+ [09-thread-coap-relay.md](./documentation/09-thread-coap-relay.md)
123
+ for what's confirmed and what isn't yet.
124
+ - **Reboot a device** (`reboot()`) — a WRITE, matching the app's own
125
+ "Restart" button exactly. Confirmed against real hardware directly
126
+ connected; not yet confirmed via relay.
107
127
  - **Low-level protocol access** (`build_frame`, `get_attribute`,
108
128
  `set_attribute`, ...) if you want to go beyond what's wrapped in
109
129
  `MobiusDevice`.
@@ -113,13 +133,22 @@ mobius-scan --adapter hci0
113
133
  | PrimitiveType | Support | Notes |
114
134
  |---|---|---|
115
135
  | `VisualV1` (Radion, Prime, Hydra, etc.) | ✅ Verified | Lights |
116
- | `VorTechV1`, `PumpV1`, `VectraV1`, `AlpacaV1`, `TurtleV1` | ✅ Verified | Pumps |
136
+ | `VorTechV1`, `TurtleV1` (AquaIllumination Axis) | ✅ Verified | Pumps -- confirmed against real hardware directly (VorTech MP40QD, AquaIllumination Axis 20) |
137
+ | `PumpV1`, `VectraV1`, `AlpacaV1` (AquaIllumination Orbit) | ✅ Verified | Pumps -- same wire format as the primitives above, not independently confirmed against their own real hardware |
117
138
  | `CoffeeV1` (NYOS Quantum) | ⚠️ Experimental | Same wire structure as pumps per the protocol, untested against real hardware |
118
139
  | `DoseV1`, `HotSauceV1` | ❌ Unsupported | Different primitive format; identity info only |
119
140
 
120
141
  `MobiusDevice.get_device_summary()` always tells you which tier applies via
121
142
  its `"support"` field — see [`documentation/04-device-identity.md`](./documentation/04-device-identity.md).
122
143
 
144
+ "Verified" above is about core telemetry/schedule parsing. The
145
+ Vectra/NYOS Quantum-specific settings methods
146
+ (`get_vectra_info()`/`get_coffee_info()`) and `motor_power_watts` are
147
+ newer, implemented from the decompiled source alone, and not verified
148
+ against real Vectra or NYOS Quantum hardware regardless of the table
149
+ above — see
150
+ [known gaps](./documentation/10-known-gaps-and-open-questions.md).
151
+
123
152
  ## Development
124
153
 
125
154
  ```bash
@@ -17,12 +17,18 @@ inferred/experimental.
17
17
  ## Status
18
18
 
19
19
  Alpha. Core protocol (framing, CRC, attribute get/set, scenes), pump
20
- telemetry, pump schedules, light schedules, and device discovery/grouping
21
- are implemented and verified against real hardware (two VorTech MP40QD
22
- pumps, two Radion XR15 G6 Pro lights). See
20
+ telemetry, pump schedules, light schedules, device discovery/grouping
21
+ (both EcoTech Marine's and AquaIllumination's own BLE company IDs), and
22
+ Thread/CoAP relay (reading a non-gateway tank member through the
23
+ gateway's own connection) are implemented and verified against real
24
+ hardware (two VorTech MP40QD pumps, two Radion XR15 G6 Pro lights, one
25
+ AquaIllumination Axis 20 pump). Two write operations are also confirmed
26
+ against real hardware: rebooting a device, and syncing a device's own
27
+ clock to the current time. See
23
28
  [`documentation/10-known-gaps-and-open-questions.md`](./documentation/10-known-gaps-and-open-questions.md)
24
- for what isn't covered yet (dosers, environmental sensors, Thread/CoAP
25
- device relay).
29
+ for what isn't covered yet (dosers, environmental sensors) or is
30
+ implemented but not yet verified against real hardware (Vectra/NYOS
31
+ Quantum-specific settings).
26
32
 
27
33
  ## Install
28
34
 
@@ -64,14 +70,28 @@ mobius-scan --adapter hci0
64
70
  - **Discover devices** and group them by tank/mesh (`pan_id`), reading
65
71
  model/serial straight from BLE advertisements — no connection required.
66
72
  - **Read pump telemetry**: current speed, estimated flow (GPH), operation
67
- state, error state.
73
+ state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts.
68
74
  - **Read pump schedules**: which mode (constant speed, tidal swell, pulse,
69
75
  etc.) is active at any given time, exactly as programmed.
70
76
  - **Read light schedules**: per-channel intensity at any given time,
71
77
  replicating the app's own client-side interpolation (there's no "current
72
78
  intensity" attribute — lights only expose the programmed curve).
79
+ - **Read device-specific settings**: VorTech's own "Local Control"/"Led
80
+ Auto Dim" and Radion's own "Max Fan Speed"/"Fan Shutdown"
81
+ (`get_advanced_features()`), plus Vectra and NYOS Quantum settings
82
+ (`get_vectra_info()`/`get_coffee_info()` — no real hardware to verify
83
+ either against, see
84
+ [known gaps](./documentation/10-known-gaps-and-open-questions.md)).
73
85
  - **Control scenes**: start feed mode, resume the normal schedule, or any
74
86
  other configured scene.
87
+ - **Fix a desynced device clock** (`set_time_to_now()`) — a WRITE.
88
+ Writing to one device appears to propagate to the rest of its Thread
89
+ mesh too, confirmed against real hardware — see
90
+ [09-thread-coap-relay.md](./documentation/09-thread-coap-relay.md)
91
+ for what's confirmed and what isn't yet.
92
+ - **Reboot a device** (`reboot()`) — a WRITE, matching the app's own
93
+ "Restart" button exactly. Confirmed against real hardware directly
94
+ connected; not yet confirmed via relay.
75
95
  - **Low-level protocol access** (`build_frame`, `get_attribute`,
76
96
  `set_attribute`, ...) if you want to go beyond what's wrapped in
77
97
  `MobiusDevice`.
@@ -81,13 +101,22 @@ mobius-scan --adapter hci0
81
101
  | PrimitiveType | Support | Notes |
82
102
  |---|---|---|
83
103
  | `VisualV1` (Radion, Prime, Hydra, etc.) | ✅ Verified | Lights |
84
- | `VorTechV1`, `PumpV1`, `VectraV1`, `AlpacaV1`, `TurtleV1` | ✅ Verified | Pumps |
104
+ | `VorTechV1`, `TurtleV1` (AquaIllumination Axis) | ✅ Verified | Pumps -- confirmed against real hardware directly (VorTech MP40QD, AquaIllumination Axis 20) |
105
+ | `PumpV1`, `VectraV1`, `AlpacaV1` (AquaIllumination Orbit) | ✅ Verified | Pumps -- same wire format as the primitives above, not independently confirmed against their own real hardware |
85
106
  | `CoffeeV1` (NYOS Quantum) | ⚠️ Experimental | Same wire structure as pumps per the protocol, untested against real hardware |
86
107
  | `DoseV1`, `HotSauceV1` | ❌ Unsupported | Different primitive format; identity info only |
87
108
 
88
109
  `MobiusDevice.get_device_summary()` always tells you which tier applies via
89
110
  its `"support"` field — see [`documentation/04-device-identity.md`](./documentation/04-device-identity.md).
90
111
 
112
+ "Verified" above is about core telemetry/schedule parsing. The
113
+ Vectra/NYOS Quantum-specific settings methods
114
+ (`get_vectra_info()`/`get_coffee_info()`) and `motor_power_watts` are
115
+ newer, implemented from the decompiled source alone, and not verified
116
+ against real Vectra or NYOS Quantum hardware regardless of the table
117
+ above — see
118
+ [known gaps](./documentation/10-known-gaps-and-open-questions.md).
119
+
91
120
  ## Development
92
121
 
93
122
  ```bash
@@ -79,14 +79,18 @@ against real pumps and lights returned correctly-structured, sane data.
79
79
  |---|---|---|
80
80
  | `Model` | 4 | See [04-device-identity.md](./04-device-identity.md) |
81
81
  | `Name` | 5 | User-assigned device name |
82
+ | `Reset` | 6 | Write `ResetType.Soft` (0) to reboot -- see [09-thread-coap-relay.md](./09-thread-coap-relay.md) |
82
83
  | `PrimitiveType` | 8 | Device class (pump/light/etc) — see 04 |
83
- | `PhysicalValues` | 101 | Sub-indexed by `PhysicalValueID`; used for live pump GPH |
84
+ | `PhysicalValues` | 101 | Sub-indexed by `PhysicalValueID`; used for live pump GPH and (Vectra/NYOS Quantum) `MotorPower`/watts -- see [11-device-status-attributes.md](./11-device-status-attributes.md) |
84
85
  | `MACAddress` | 103 | Observed unpopulated (empty response) on all devices tested |
85
86
  | `OperationState` | 104 | `OOB`/`LiveDemo`/`Scene`/`Schedule` |
86
87
  | `ErrorState` | 107 | See [04-device-identity.md](./04-device-identity.md) |
87
88
  | `CurrentScene` | 401 | Set to `sceneId(u16 LE) + durationMinutes(u16 LE)` to start a scene |
88
89
  | `Schedule1` / `Schedule2` | 500 / 503 | The programmed point-schedule — see 06/07 |
89
- | `MotorSpeed` | 700 | Live pump speed (int16). Confirmed NOT RPM -- percentage of max power in tenths of a percent (abs(value)/10 = %); sign encodes rotation direction. Confirmed via the app's own display code, `String.format("%.0f%%", Math.abs(currentSpeed / 10.0f))`. |
90
+ | `MotorSpeed` | 700 | Live pump speed (int16). Confirmed NOT RPM -- percentage of max power in tenths of a percent (abs(value)/10 = %); sign encodes rotation direction. Confirmed via the app's own display formatting. |
91
+ | `LocalControlEnabled` / `AutoDimTimeout` / `MaxFanSpeed` / `FanShutdownEnabled` | 300 / 301 / 910 / 1203 | VorTech/Radion advanced feature settings — see 11 |
92
+ | `PowerOnDelay` / `ClosedLoop` / `FeedModeReturnDelay` | 800 / 801 / 802 | Vectra settings — see 11 |
93
+ | `CoffeeLedOn` / `CoffeeStatusIntensity` / `CoffeeFeedModeReturnDelay` / `CoffeePowerOnDelay` | 717 / 4100 / 4101 / 4102 | NYOS Quantum settings — see 11 |
90
94
  | `SupportedColorChannels` | 901 | Static list of a light's channels |
91
95
 
92
96
  See `src/mobius/constants.py` for the complete implemented list.
@@ -22,7 +22,7 @@ way to tell devices apart over BLE (they all advertise the same local name,
22
22
 
23
23
  ### `PRIMITIVE_SIZE` — schedule-point primitive byte size, per type
24
24
 
25
- Confirmed from `M.PrimitiveType.size()`. For `VisualV1` this is a
25
+ Confirmed from the app's own per-primitive size lookup. For `VisualV1` this is a
26
26
  per-channel repeating unit (3 bytes: `VisualID` + `u16` intensity), not a
27
27
  fixed total. For everything else, it's the exact fixed size of that
28
28
  primitive type's data.
@@ -5,7 +5,7 @@ override the normal schedule temporarily or permanently.
5
5
 
6
6
  ## `SceneID`
7
7
 
8
- Confirmed literal values from `M.SceneID`:
8
+ Confirmed literal values:
9
9
 
10
10
  | Value | Name |
11
11
  |---|---|
@@ -45,7 +45,7 @@ back to schedule-driven.
45
45
 
46
46
  ## `OperationState`
47
47
 
48
- Confirmed literal values from `M.OperationState`:
48
+ Confirmed literal values:
49
49
 
50
50
  | Value | Name |
51
51
  |---|---|
@@ -165,19 +165,17 @@ point. In practice this covers essentially the whole night, *except*
165
165
  the two transition segments themselves: dusk (start point not yet
166
166
  night-flagged) and the sunrise segment (start point flagged `SUNRISE`).
167
167
 
168
- **Confirmed directly against the raw bytecode, not the decompiled Java.**
169
- An earlier decompilation attempt for this
170
- exact method was unreliable -- a real signal, not noise: the decompiled Java had this
171
- condition's first check inverted (`!point.has(NIGHT)` instead of the
172
- actual `point.has(NIGHT)`), which an earlier version of this port
173
- carried forward uncorrected. Found via real hardware testing: a light
174
- in a `23:00[NIGHT,SUNSET] -> 23:27[NIGHT]` segment showed a value
175
- consistent with the schedule-intensity scalar from this library, while
176
- the real app displayed a noticeably lower, lunar-consistent value at
177
- the same moment -- mathematically impossible to explain by
178
- schedule-intensity alone within that segment's raw-value range, which
179
- is what prompted re-examining the raw bytecode instead of trusting the
180
- decompiled Java a second time.
168
+ **Confirmed directly against the raw bytecode, not the decompiled
169
+ source.** The decompiled source for this exact method is unreliable --
170
+ a real signal, not noise: it has this condition's first check inverted
171
+ (`!point.has(NIGHT)` instead of the actual `point.has(NIGHT)`). Found
172
+ via real hardware testing: a light in a `23:00[NIGHT,SUNSET] ->
173
+ 23:27[NIGHT]` segment showed a value consistent with the schedule-
174
+ intensity scalar from this library, while the real app displayed a
175
+ noticeably lower, lunar-consistent value at the same moment --
176
+ mathematically impossible to explain by schedule-intensity alone within
177
+ that segment's raw-value range, which is what prompted re-examining the
178
+ raw bytecode instead of trusting the decompiled source a second time.
181
179
 
182
180
  **Acclimation ramp** (`AcclimationInfo`): a linear intensity ramp from
183
181
  `start_intensity` (0-1000 permille) up to full (1000) over `period_days`,
@@ -189,6 +187,36 @@ completed yet.
189
187
  `get_current_light_intensities()` fetches and applies all of this
190
188
  automatically -- no separate calls needed for the common case.
191
189
 
190
+ **`get_lunar_phase_info()`**: reads `LunarPhasesCurrentDay`/
191
+ `LunarPhasesCurrentScalar` directly off the device -- its own
192
+ firmware-computed lunar state, confirmed against real hardware to
193
+ match `LUNAR_CYCLE_SCALERS[current_day]` exactly, even though the app
194
+ itself never reads either attribute (it always computes the
195
+ equivalent value client-side, the same way this library does). Useful
196
+ as an independent cross-check against this library's own calculated
197
+ value.
198
+
199
+ **A confirmed bug in the app itself, not in this library**: the app's
200
+ own `date`-to-calendar-day conversion
201
+ (`LunarInfo.jdFromGregorianDate()`) applies the phone's own local
202
+ timezone via `Calendar.getInstance()` on top of a value
203
+ (`Tank.getDateTime().date()`) that's already timezone-adjusted once
204
+ (the device's own `Epoch`-to-`LocalTime` offset). This double
205
+ application means the app's own displayed lunar value rolls over to
206
+ the next calendar day exactly one UTC-offset early, relative to the
207
+ device's true local midnight (assuming the phone and device agree on
208
+ timezone, the common case) -- confirmed via the exact arithmetic and
209
+ via the general symptom matching (a real, reported case of the app
210
+ showing the next day's value visibly before midnight, on a
211
+ device/phone at UTC+1, consistent with this mechanism). The precise
212
+ one-hour timing itself is a derived prediction from the arithmetic,
213
+ not something separately checked against a clock. This library's own calculation (and the device's own
214
+ firmware, per `get_lunar_phase_info()` above) doesn't have this bug --
215
+ both correctly roll over at true local midnight. A mismatch between
216
+ this library's own calculated lunar value and the app's own displayed
217
+ one, specifically around midnight, is expected and doesn't indicate
218
+ anything wrong with this library.
219
+
192
220
  ## Insolation detection (flagged, not implemented)
193
221
 
194
222
  Insolation itself is not implemented (see below), but
@@ -3,8 +3,8 @@
3
3
  ## Same attribute, same `Point` framing, different primitive and different semantics
4
4
 
5
5
  Pumps use the exact same `Schedule1`/`Schedule2` attributes and the exact
6
- same `time + flags + primitiveData` point framing as lights (confirmed:
7
- `Schedule.checkSupport()` only checks whether `Schedule1` is a supported
6
+ same `time + flags + primitiveData` point framing as lights (confirmed: the
7
+ app's own support check only checks whether `Schedule1` is a supported
8
8
  attribute at all — it isn't gated by `PrimitiveType`). What differs:
9
9
 
10
10
  - `primitiveData` decodes as a **`PumpPrimitive`**, not a `LightPrimitive`.
@@ -27,7 +27,7 @@ params (variable, depends on mode)
27
27
 
28
28
  ### `PumpMode`
29
29
 
30
- Confirmed literal values from `M.PumpMode` (note the gap: value `11` is
30
+ Confirmed literal values (note the gap: value `11` is
31
31
  genuinely unused in the source):
32
32
 
33
33
  | Value | Name |
@@ -51,9 +51,9 @@ genuinely unused in the source):
51
51
 
52
52
  ### Parameters per mode
53
53
 
54
- Ported directly from `M.PumpMode.parameters()` — which fields are present,
55
- and in what order (byte offset is cumulative, starting right after the mode
56
- byte):
54
+ Ported directly from the app's own pump-mode parameter logic — which fields
55
+ are present, and in what order (byte offset is cumulative, starting right
56
+ after the mode byte):
57
57
 
58
58
  | Mode | Parameters |
59
59
  |---|---|
@@ -97,7 +97,7 @@ matters for your use case.
97
97
 
98
98
  ### `RampType`
99
99
 
100
- Confirmed literal values from `M.RampType`:
100
+ Confirmed literal values:
101
101
 
102
102
  | Value | Name |
103
103
  |---|---|
@@ -6,8 +6,22 @@ required.** This is the fastest way to discover and group devices.
6
6
 
7
7
  ## Company ID
8
8
 
9
- `0x0202`, confirmed via `bleak`'s `AdvertisementData.manufacturer_data`
10
- dict against all four devices tested.
9
+ Two confirmed, via `bleak`'s `AdvertisementData.manufacturer_data` dict:
10
+
11
+ - `0x0202` (514) — EcoTech Marine devices, confirmed against all four
12
+ original test devices (two VorTech pumps, two Radion lights).
13
+ - `0x0001` (1) — AquaIllumination devices, confirmed against a real
14
+ captured AI Axis 20 pump advertisement: the decoded `model` field
15
+ (261) matches `Model.Axis20` exactly (the app's own internal enum
16
+ name for it is `Turtle1`), and the decoded serial matched the
17
+ device's own GATT-read serial. Whether Neptune Systems/NYOS (also
18
+ EcoTech-family brands) share `0x0202` or use their own separate ID
19
+ is inferred, not directly confirmed — no advertisement from either
20
+ has actually been captured and checked.
21
+
22
+ Both confirmed company IDs use the exact same 23-byte payload format
23
+ described below — `parse_manufacturer_data()` doesn't need to know or
24
+ care which company ID a payload came from.
11
25
 
12
26
  ## Payload format
13
27
 
@@ -17,10 +31,12 @@ library's `parse_manufacturer_data()` takes the already-stripped 23-byte
17
31
  `bleak` payload directly, which corresponds to `javaBArr[2:]`.
18
32
 
19
33
  The decompile has several format branches keyed by total length and a
20
- "version" byte; **only the branch actually observed on real hardware is
21
- implemented** (23-byte `bleak` payload / 25-byte original Java `bArr`
22
- length, "version 2"). Payloads of any other length return `None` rather
23
- than guessing at an unimplemented branch.
34
+ "version" byte (the company ID's own low byte -- see "Company ID" above);
35
+ **only the branch actually observed on real hardware is implemented**
36
+ (23-byte `bleak` payload / 25-byte original Java `bArr` length) --
37
+ confirmed identical for both company IDs' own "version 1"/"version 2"
38
+ variant of this specific length. Payloads of any other length return
39
+ `None` rather than guessing at an unimplemented branch.
24
40
 
25
41
  ```
26
42
  offset size field maps to bleak payload offset
@@ -55,13 +71,22 @@ payload, i.e. what `parse_manufacturer_data()` actually receives.)
55
71
  `MobiusAdvertisement.device_specific_data` for completeness/future use.
56
72
  - **The unused byte at offset 2**: genuinely dead — grepped the entire
57
73
  decompile for any code path reading this offset (in either the "version
58
- 1" or "version 2" format branch) and found none. Constant `0x01` in every
59
- sample. Not exposed as a field since there's nothing to name it.
74
+ 1" or "version 2" format branch) and found none for the EcoTech-family
75
+ branch; the AquaIllumination branch does read it into a local variable
76
+ (`b2`) but never uses it afterward either. Constant `0x01` in every
77
+ sample observed, EcoTech and AquaIllumination alike. Not exposed as a
78
+ field since there's nothing to name it.
60
79
 
61
80
  ## "Flags" — deliberately not exposed
62
81
 
63
- The decompile's version-2 branch also builds a `Flags` object from
64
- `javaBArr[1]` — but in this format, that byte is actually the company ID's
65
- *high byte* (`0x02`, constant), not real payload data. Decoding "flags"
66
- here would just be re-decoding the company ID and calling it something
67
- else, so this library doesn't expose it.
82
+ `javaBArr[1]` is the company ID's own *high byte* for EcoTech Marine
83
+ (`0x0202`) that's a constant `0x02`; for AquaIllumination (`0x0001`)
84
+ it's a constant `0x00`. The decompile still gives this byte its own,
85
+ different treatment per company ID: the EcoTech branch just wraps it
86
+ in an opaque `Flags` object with no further use shown; the
87
+ AquaIllumination branch reads it as `pendingNotifications = byte > 0`
88
+ (always `False`, since the byte is a constant `0x00`). Either way,
89
+ it's not independent payload data — decoding "flags" here would just
90
+ be re-decoding a constant part of the company ID and calling it
91
+ something else, so this library doesn't expose it for either company
92
+ ID.
@@ -121,6 +121,83 @@ carries its own CRC, which is what's actually checked.
121
121
  frame without waiting for its confirm, and awaits the token-matched
122
122
  indication with a timeout.
123
123
 
124
+ Retries exactly once on a timeout (confirmed via reverse engineering the
125
+ app's own request-timeout handling -- it never treats a single timed-out
126
+ relay round-trip as a real failure) before actually raising. After both
127
+ attempts have timed out, every later call on that SAME
128
+ `RelayedMobiusDevice` instance fails immediately instead of repeating
129
+ the same two-attempt wait -- a fresh instance is created each poll
130
+ cycle, and a caller like `get_device_summary()` makes several separate
131
+ attribute reads against one instance in immediate succession; if the
132
+ mesh route to that target is down, it's down for all of them, not just
133
+ whichever one happened to be tried first.
134
+
135
+ ## Time sync (`set_time_to_now()`) -- writing to one device propagates to the rest of the mesh
136
+
137
+ `set_time_to_now()` writes `Epoch` to a single device, with the frame's
138
+ own reserved-byte "group" field set to 1 (see `make_reserved()`) --
139
+ confirmed via the app's own source to be set unconditionally on every
140
+ time-related write it ever sends. Confirmed against real hardware:
141
+ `group=1` is required for the write to be accepted at all -- `group=0`
142
+ fails the write entirely, even on the device it targets directly.
143
+
144
+ This single write appears to genuinely propagate to OTHER devices on
145
+ the same mesh that were never written to directly -- confirmed via real
146
+ test data: three mesh peers (read via relay, none of them the device
147
+ actually written to) disagreed with each other by 1 second before the
148
+ write; seconds after a single write to a fourth, different device, all
149
+ three read back an identical timestamp, despite each having a different
150
+ amount of real time elapsed since its own prior read. Independent
151
+ clocks coincidentally landing on the exact same value, with different
152
+ elapsed times each, isn't a plausible explanation for that -- this is
153
+ strong evidence of real propagation, not just normal ticking.
154
+
155
+ Not confirmed: reliability across different mesh topologies or mesh
156
+ sizes, behavior under real-world conditions (e.g. a peer briefly
157
+ unreachable when propagation happens), or whether writing via
158
+ `RelayedMobiusDevice` (rather than directly, as tested) behaves the
159
+ same way -- see `set_time_to_now()`'s own docstring for the full
160
+ breakdown.
161
+
162
+ ## Device control (`reboot()`) -- targeted, no mesh-wide propagation
163
+
164
+ `reboot()` writes `ResetType.Soft` (0) to `Reset` (6) -- confirmed via
165
+ the app's own source to be exactly what its own "Restart" button
166
+ sends. Unlike `set_time_to_now()`, this is a plain, ungrouped write
167
+ (no reserved-byte `group` field involved at all) -- it targets exactly
168
+ the device it's sent to, nothing else, even though (like any other
169
+ request) it can still be relayed through a gateway to reach that
170
+ target in the first place.
171
+
172
+ Confirmed against real hardware, directly connected. Confirmed via the
173
+ app's own request-routing logic that a `Reset` write isn't
174
+ special-cased at all -- it goes through the exact same generic routing
175
+ every other Get/Set uses, including the same relay branch for a target
176
+ that isn't the directly-connected device (the app wraps the request's
177
+ own raw FSCI payload, `Set` opcode included, inside a CoAP request
178
+ addressed to the target's mesh address, and sends that to the
179
+ gateway). This library's own `RelayedMobiusDevice` already implements
180
+ that same mechanism, and `reboot()` is built on it transparently, the
181
+ same way every other high-level method is -- reasoned to work via
182
+ relay for the same structural reason, but not itself confirmed against
183
+ real hardware via relay yet (`RelayedMobiusDevice`'s own reads are
184
+ confirmed against real hardware extensively; no write through it, this
185
+ one or `set_time_to_now()`, has actually been tried against real
186
+ hardware yet).
187
+
188
+ Also confirmed via the app's own source: the device sends a normal
189
+ FSCI success response BEFORE actually rebooting (the app's own
190
+ handler just disconnects client-side once that confirm arrives,
191
+ rather than waiting for the device to drop the connection on its
192
+ own) -- `reboot()` returning successfully means the device
193
+ ACKNOWLEDGED the request, not necessarily that it has already
194
+ finished rebooting.
195
+
196
+ Only `ResetType.Soft` is exposed -- see that method's own docstring
197
+ for why the app's other `Reset` values (`Factory`, `PowerLoss`,
198
+ `PowerRecovery`, `Calibration`) deliberately aren't wired up to
199
+ anything.
200
+
124
201
  ## Address discovery
125
202
 
126
203
  A device's Thread mesh-local RLOC (Routing Locator) IPv6 address:
@@ -18,6 +18,42 @@ included as a starting point.
18
18
 
19
19
  ## Experimental / unverified
20
20
 
21
+ - **`reboot()` via relay**: confirmed against real hardware directly
22
+ connected. Not confirmed via `RelayedMobiusDevice` -- no write
23
+ through it, this one or `set_time_to_now()`, has been tried against
24
+ real hardware yet. See
25
+ [09-thread-coap-relay.md](./09-thread-coap-relay.md).
26
+
27
+ - **`set_time_to_now()` via `RelayedMobiusDevice`, and across different
28
+ mesh topologies/sizes**: the write itself and its propagation to
29
+ other mesh members are confirmed against real hardware (one tank,
30
+ writing directly to a connected device) -- see
31
+ [09-thread-coap-relay.md](./09-thread-coap-relay.md). Not tested:
32
+ writing via relay to a non-gateway member, larger meshes, or
33
+ real-world conditions like a peer being briefly unreachable when
34
+ propagation happens.
35
+
36
+ - **`get_advanced_features()`** (`AdvancedFeatures`): implemented from
37
+ the decompile; not checked against real VorTech or Radion hardware
38
+ specifically for these four attributes (`LocalControlEnabled`/
39
+ `AutoDimTimeout`/`MaxFanSpeed`/`FanShutdownEnabled`), unlike the rest
40
+ of this library's pump/light support.
41
+ - **`get_vectra_info()`/`get_coffee_info()`** (`VectraInfo`/
42
+ `CoffeeInfo`): no real Vectra or NYOS Quantum hardware was available
43
+ to verify either against -- implemented from the decompile alone.
44
+ Both also each have one attribute ID (`PowerOnDelay`/
45
+ `CoffeeStatusIntensity`) that needed external confirmation, since
46
+ they resolve in the decompile through third-party Android library
47
+ constants (an animation library, an AndroidX SDK constant
48
+ respectively) rather than plain literals in the app's own code --
49
+ see [11-device-status-attributes.md](./11-device-status-attributes.md)
50
+ for the full confirmation.
51
+ - **`motor_power_watts`** (`get_pump_telemetry()`): confirmed via the
52
+ app's own gauge-widget logic to be the value it falls back to,
53
+ relabeled "watts", for Vectra/NYOS Quantum pumps specifically when
54
+ its own flow-range display object isn't populated for that device --
55
+ but not checked against real Vectra/NYOS Quantum hardware. Purely
56
+ additive; doesn't affect `speed`/`gph` for any device.
21
57
  - **`CoffeeV1` (NYOS Quantum skimmers/dosers)**: shares the exact declared
22
58
  primitive size (13 bytes) with pump-class devices, so this library
23
59
  applies the pump parser automatically — but this has never been tested