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.
- {python_mobius-0.4.3 → python_mobius-0.5.0}/CHANGELOG.md +52 -3
- {python_mobius-0.4.3 → python_mobius-0.5.0}/PKG-INFO +37 -8
- {python_mobius-0.4.3 → python_mobius-0.5.0}/README.md +36 -7
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/03-attributes-and-opcodes.md +6 -2
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/04-device-identity.md +1 -1
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/05-scenes.md +2 -2
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/06-light-schedule.md +41 -13
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/07-pump-schedule.md +7 -7
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/08-manufacturer-data.md +38 -13
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/09-thread-coap-relay.md +77 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/10-known-gaps-and-open-questions.md +36 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/11-device-status-attributes.md +103 -2
- {python_mobius-0.4.3 → python_mobius-0.5.0}/pyproject.toml +1 -1
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/__init__.py +5 -5
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/cli.py +98 -6
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/constants.py +311 -91
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/device.py +492 -61
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/device_status.py +146 -6
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/discovery.py +11 -4
- python_mobius-0.5.0/src/mobius/manufacturer.py +72 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/modifiers.py +6 -6
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/relay.py +37 -2
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/schedule.py +5 -6
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_connection_cleanup.py +61 -2
- python_mobius-0.5.0/tests/test_device_control.py +59 -0
- python_mobius-0.5.0/tests/test_device_status.py +373 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_device_summary.py +47 -2
- python_mobius-0.5.0/tests/test_device_time.py +241 -0
- python_mobius-0.5.0/tests/test_discovery_company_ids.py +107 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_firmware_versions.py +82 -6
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_light_intensity_branching.py +125 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_light_schedule.py +1 -3
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_manufacturer.py +26 -0
- python_mobius-0.5.0/tests/test_manufacturer_lookup.py +57 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_modifiers.py +1 -2
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_networked_thread_devices.py +13 -16
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_pump_schedule.py +5 -5
- python_mobius-0.5.0/tests/test_pump_telemetry.py +133 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_relay.py +132 -1
- python_mobius-0.4.3/src/mobius/manufacturer.py +0 -53
- python_mobius-0.4.3/tests/test_device_status.py +0 -167
- python_mobius-0.4.3/tests/test_device_time.py +0 -125
- python_mobius-0.4.3/tests/test_manufacturer_lookup.py +0 -26
- {python_mobius-0.4.3 → python_mobius-0.5.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/.gitignore +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/LICENSE +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/00-overview.md +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/coap.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/crc.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/frame.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/mesh_address.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/power.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_coap.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_frame.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_power.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.4.3 → python_mobius-0.5.0}/tests/test_serial_decoding.py +0 -0
- {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
|
-
|
|
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.
|
|
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,
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
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`, `
|
|
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,
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
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`, `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
55
|
-
and in what order (byte offset is cumulative, starting right
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
{python_mobius-0.4.3 → python_mobius-0.5.0}/documentation/10-known-gaps-and-open-questions.md
RENAMED
|
@@ -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
|