python-mobius 0.1.1__tar.gz → 0.1.3__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.1.3/CHANGELOG.md +233 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/PKG-INFO +2 -1
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/00-overview.md +2 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/03-attributes-and-opcodes.md +1 -1
- python_mobius-0.1.3/documentation/06-light-schedule.md +216 -0
- python_mobius-0.1.3/documentation/11-device-status-attributes.md +166 -0
- python_mobius-0.1.3/documentation/12-device-identity-and-address-stability.md +114 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/pyproject.toml +3 -2
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/__init__.py +27 -5
- python_mobius-0.1.3/src/mobius/cli.py +125 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/constants.py +103 -0
- python_mobius-0.1.3/src/mobius/device.py +1080 -0
- python_mobius-0.1.3/src/mobius/device_status.py +89 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/discovery.py +53 -0
- python_mobius-0.1.3/src/mobius/modifiers.py +167 -0
- python_mobius-0.1.3/src/mobius/power.py +90 -0
- python_mobius-0.1.3/src/mobius/pump_status.py +50 -0
- python_mobius-0.1.3/tests/test_connection_cleanup.py +107 -0
- python_mobius-0.1.3/tests/test_device_status.py +167 -0
- python_mobius-0.1.3/tests/test_device_summary.py +158 -0
- python_mobius-0.1.3/tests/test_firmware_versions.py +141 -0
- python_mobius-0.1.3/tests/test_hyperdrive_device.py +172 -0
- python_mobius-0.1.3/tests/test_insolation_detection.py +133 -0
- python_mobius-0.1.3/tests/test_light_intensity_branching.py +139 -0
- python_mobius-0.1.3/tests/test_modifiers.py +156 -0
- python_mobius-0.1.3/tests/test_power.py +101 -0
- python_mobius-0.1.3/tests/test_pump_status.py +161 -0
- python_mobius-0.1.3/tests/test_schedule_intensity_scalar.py +111 -0
- python_mobius-0.1.3/tests/test_serial_identity.py +214 -0
- python_mobius-0.1.1/CHANGELOG.md +0 -46
- python_mobius-0.1.1/documentation/06-light-schedule.md +0 -90
- python_mobius-0.1.1/src/mobius/cli.py +0 -68
- python_mobius-0.1.1/src/mobius/device.py +0 -449
- {python_mobius-0.1.1 → python_mobius-0.1.3}/.gitignore +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/LICENSE +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/README.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/05-scenes.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/09-thread-coap-relay.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/crc.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/frame.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/src/mobius/schedule.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_frame.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.1.1 → python_mobius-0.1.3}/tests/test_serial_decoding.py +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
- **Added firmware/hardware version reading.** `get_firmware_versions()`
|
|
6
|
+
(`FirmwareVersion`, attribute 1) with **confirmed-against-real-hardware**
|
|
7
|
+
display labels for EcoTech Marine devices (a real VorTech pump showed
|
|
8
|
+
exactly Radio/Radio Bootloader/Product OS/Product Bootloader; a real
|
|
9
|
+
Radion light showed exactly Product OS/Product Bootloader/Radio
|
|
10
|
+
Firmware/Filesystem/Radio OS/Radio/WLAN -- confirming lights use a
|
|
11
|
+
QCA4020-based radio with WiFi+BLE, unlike the simpler pump radio).
|
|
12
|
+
Ported the exact label mapping from `FirmwareType.name(Model)` in the
|
|
13
|
+
decompile. Versions are dot-joined strings built directly from the raw
|
|
14
|
+
response bytes (confirmed via `FirmwareType.format()`). Also added
|
|
15
|
+
`get_hardware_info()` (`HardwareRevision`, attribute 2, same fetch
|
|
16
|
+
pattern) -- returns raw bytes, no confirmed display format for those
|
|
17
|
+
fields. `get_device_summary()` now includes `"firmware_versions"`
|
|
18
|
+
automatically. 6 new tests.
|
|
19
|
+
|
|
20
|
+
- **Fixed a real CLI bug**: passing both a scan-time `BLEDevice` and
|
|
21
|
+
`serial=` into `MobiusDevice` in the connect loop did NOT actually give
|
|
22
|
+
serial-based robustness -- a directly-provided `BLEDevice` is always
|
|
23
|
+
used on the first connect, and the CLI only calls `connect()` once per
|
|
24
|
+
device, so the serial fallback path never triggered. That scan-time
|
|
25
|
+
`BLEDevice` could be tens of seconds stale by the time the loop reaches
|
|
26
|
+
the last device in a long scan -- confirmed against real hardware as the
|
|
27
|
+
cause of `"device disappeared"` errors. Fixed: the CLI now passes
|
|
28
|
+
`serial=` alone (omitting `device`) whenever available, forcing a fresh
|
|
29
|
+
resolution immediately before each connect.
|
|
30
|
+
|
|
31
|
+
- **Major fix: device identity is now serial-number-based, not BLE-address-based.**
|
|
32
|
+
Real-world testing surfaced a device whose advertised MAC address changed
|
|
33
|
+
between runs. Investigating the decompiled app's own `Peripheral` class
|
|
34
|
+
confirmed the official app never relies on address stability at all --
|
|
35
|
+
`Peripheral.equals()`/`hashCode()` are based purely on serial number, and
|
|
36
|
+
`Peripheral.update()` explicitly replaces the stored BLE address whenever
|
|
37
|
+
a new scan matches an existing device by serial. This library previously
|
|
38
|
+
assumed address stability throughout.
|
|
39
|
+
- New `find_device_by_serial()` and `dedupe_by_serial()`
|
|
40
|
+
(`mobius.discovery`).
|
|
41
|
+
- `MobiusDevice(device=None, serial=None, ...)`: `serial` is a new,
|
|
42
|
+
fully backward-compatible optional parameter. A directly-provided
|
|
43
|
+
`BLEDevice` is still used for the fast first connect, but if `serial`
|
|
44
|
+
is also set, any *subsequent* `connect()` on the same instance
|
|
45
|
+
re-resolves the current address via serial rather than assuming the
|
|
46
|
+
original address/BLEDevice is still valid. Existing address-only
|
|
47
|
+
usage is completely unchanged.
|
|
48
|
+
- `mobius-scan` CLI: applies `dedupe_by_serial()` automatically, passes
|
|
49
|
+
`serial=` when connecting, and has a new `--by-serial SERIAL` flag for
|
|
50
|
+
testing this directly against real hardware.
|
|
51
|
+
- 12 new tests. See
|
|
52
|
+
`documentation/12-device-identity-and-address-stability.md` for the
|
|
53
|
+
full evidence and rationale.
|
|
54
|
+
- Not yet applied to `ha-mobius`, which has the same address-based
|
|
55
|
+
assumption -- planned as a follow-up once this is confirmed working
|
|
56
|
+
against real hardware.
|
|
57
|
+
|
|
58
|
+
- **Fixed a real bug**: `get_device_summary()` -- the "read everything"
|
|
59
|
+
entrypoint the CLI and other callers actually use -- was never updated
|
|
60
|
+
to include `get_pump_flow_range()`/`get_pump_override_mode()`/
|
|
61
|
+
`get_battery_backup_info()`/`get_boosted_battery_info()`/
|
|
62
|
+
`get_group_info()`/`get_calibration_info()`/`get_maintenance_info()`
|
|
63
|
+
after they were added. All 7 methods worked correctly in isolation with
|
|
64
|
+
their own passing tests, but nothing exercised `get_device_summary()`
|
|
65
|
+
itself, so the gap went undetected through 103 passing tests. Caught via
|
|
66
|
+
real CLI usage. Fixed, and `tests/test_device_summary.py` now exists
|
|
67
|
+
specifically so this class of gap can't go undetected again (confirmed:
|
|
68
|
+
reverting the fix makes the new test fail with a clear message pointing
|
|
69
|
+
at the missing key).
|
|
70
|
+
|
|
71
|
+
- **Added group/sync, calibration, and maintenance reads.** New
|
|
72
|
+
`mobius.device_status` module. `get_group_info()` (`GroupMaster`/
|
|
73
|
+
`IsGroupMaster`, 900/911 -- confirmed dead in the app itself, zero real
|
|
74
|
+
usage beyond the enum definitions). `get_calibration_info()`
|
|
75
|
+
(`IsCalibrated`/`LastCalibrationTime`/`MinCalibratedSpeed`/
|
|
76
|
+
`MaxCalibratedSpeed`, 1300/1304/1308/1309) -- **important correction**:
|
|
77
|
+
despite "Speed"-sounding names, confirmed via the app's own UI gating
|
|
78
|
+
(`DeviceSettingsFragment.java`: `device.primitive.category() ==
|
|
79
|
+
M.DeviceCategory.Lighting`) that this is a light feature, not a pump
|
|
80
|
+
one. A separate, unrelated pump-side calibration flow exists in the app
|
|
81
|
+
(Nero-specific zero-flow/max-flow calibration) but isn't implemented and
|
|
82
|
+
doesn't apply to VorTech anyway (no physical flow sensor to calibrate).
|
|
83
|
+
`get_maintenance_info()` (`RecommendedMaintenanceInterval`/
|
|
84
|
+
`MaintenanceTimer`/`LastMaintenanceTime`/`MaintenanceDue`,
|
|
85
|
+
1400-1403 -- also confirmed dead in the app; returns raw bytes rather
|
|
86
|
+
than guessing a numeric width with zero supporting evidence). 7 new
|
|
87
|
+
tests. Renamed `documentation/11-pump-status-and-capabilities.md` to
|
|
88
|
+
`documentation/11-device-status-attributes.md` to reflect the broader
|
|
89
|
+
(not pump-only) scope.
|
|
90
|
+
|
|
91
|
+
- **Added pump status/capability reads**: `get_pump_flow_range()`
|
|
92
|
+
(`MinimumGallonsPerHour`/`MaximumGallonsPerHour`, 707/708 -- confirmed
|
|
93
|
+
NOT to apply to VectraV1 pumps), `get_pump_override_mode()` (new
|
|
94
|
+
`PumpOverrideMode` enum, attribute 705 -- confidence note: confirmed
|
|
95
|
+
only as a *write* target in the decompile, never read; this library's
|
|
96
|
+
read path is a reasonable inference, not directly observed),
|
|
97
|
+
`get_battery_backup_info()` (`BatteryBackupSpeed`/`BatteryBackupMaxSpeed`,
|
|
98
|
+
706/701), `get_boosted_battery_info()` (`BoostedBatteryPower`/`OnTime`/
|
|
99
|
+
`OffTime`, 803/804/805). New `mobius.pump_status` module
|
|
100
|
+
(`PumpFlowRange`, `BatteryBackupInfo`, `BoostedBatteryInfo`). All fail
|
|
101
|
+
soft (return `None`) for unsupported/model-dependent attributes rather
|
|
102
|
+
than raising. 9 new tests. See
|
|
103
|
+
`documentation/11-device-status-attributes.md`.
|
|
104
|
+
|
|
105
|
+
- **Added full hyperdrive support** (correct percentage conversion for
|
|
106
|
+
light channels boosted above their normal 100% rating). New
|
|
107
|
+
`mobius.power` module (`ChannelPowerInfo`, `channel_percent_value()`,
|
|
108
|
+
ported from `LightPowerInfo.java`) and `get_channel_power_info()`
|
|
109
|
+
(`MaxPower`/`NormalPower`, attributes 1504/1513). New
|
|
110
|
+
`get_current_light_percentages()` convenience method returns corrected
|
|
111
|
+
0-100+ percentages instead of raw permille, flagging boosted channels via
|
|
112
|
+
`.diagnostics["hyperdrive_channels"]`. Confirmed byte-identical to the
|
|
113
|
+
old `raw/10` shortcut for the common (non-boosted) case; only changes
|
|
114
|
+
results for genuinely boosted channels. `LightPowerInfo`'s Prime/Hydra-
|
|
115
|
+
specific `gm8b4()` wattage override table is not replicated -- see
|
|
116
|
+
`documentation/06-light-schedule.md`.
|
|
117
|
+
- Also refactored `get_attribute()` into a thin wrapper around a new
|
|
118
|
+
`get_attribute_raw()`, which preserves the device-reported response
|
|
119
|
+
index -- required for `MaxPower`/`NormalPower`, where the index *is*
|
|
120
|
+
the `VisualID` byte value, not just a positional offset. Added a
|
|
121
|
+
regression test proving this distinction matters (a device that
|
|
122
|
+
starts its response at a non-zero index would have silently
|
|
123
|
+
mismatched values to the wrong channels under the old approach).
|
|
124
|
+
- 15 new tests across `tests/test_power.py` and
|
|
125
|
+
`tests/test_hyperdrive_device.py`.
|
|
126
|
+
|
|
127
|
+
- **Added Insolation detection/flagging.** Insolation itself is not
|
|
128
|
+
implemented (it's more than a missing modifier -- when active, the app
|
|
129
|
+
substitutes an entirely different, dynamically-generated schedule
|
|
130
|
+
before interpolating, not something layered on top the way lunar/
|
|
131
|
+
acclimation are). `get_current_light_intensities()` now returns a
|
|
132
|
+
`LightIntensityResult` -- a fully backward-compatible dict subclass with
|
|
133
|
+
an added `.diagnostics["insolation_active"]` flag, via a new
|
|
134
|
+
`get_insolation_enabled()` (`InsolationEnabled`, attribute 912).
|
|
135
|
+
|
|
136
|
+
- **Fixed a real accuracy bug**: `get_current_light_intensities()`
|
|
137
|
+
previously returned only the raw per-channel interpolated curve, missing
|
|
138
|
+
a schedule-level master intensity scalar (`Schedule1Intensity`, attribute
|
|
139
|
+
511) that the real app applies on top. Caught by direct comparison
|
|
140
|
+
against real app screenshots showing different numbers than this
|
|
141
|
+
library computed for the same light at the same time. Traced to
|
|
142
|
+
`WidgetViewCell.java`/`IntensityView.java` in the decompiled source.
|
|
143
|
+
Added `get_schedule_intensity()`; `get_current_light_intensities()` now
|
|
144
|
+
applies it automatically.
|
|
145
|
+
- **Added lunar-phase and acclimation-ramp support**, the two remaining
|
|
146
|
+
modifiers flagged as gaps in the previous entry. New `mobius.modifiers`
|
|
147
|
+
module, ported from `LunarInfo.java`/`AcclimationInfo.java`:
|
|
148
|
+
- `lunar_percent_reduction(date)` -- verified against real,
|
|
149
|
+
independently-confirmed 2026 moon phase dates (full moon Jan 3, new
|
|
150
|
+
moon Jan 18); the ported algorithm matches exactly.
|
|
151
|
+
- `AcclimationInfo` -- linear intensity ramp from a starting percentage
|
|
152
|
+
up to full over a configured period.
|
|
153
|
+
- `is_night_segment()` -- ported from the boolean half of
|
|
154
|
+
`PointSchedule.getIntensitiesAtTime()`; confirmed this is specifically
|
|
155
|
+
"the dusk-to-night transition segment," not simply "is it night."
|
|
156
|
+
- `get_lunar_enabled()`, `get_acclimation_info()`,
|
|
157
|
+
`get_current_light_intensities()` now applies the full branching the
|
|
158
|
+
app itself uses (night+lunar / night-no-lunar / day+active-acclimation
|
|
159
|
+
/ day-no-acclimation).
|
|
160
|
+
- "Insolation" (a real per-calendar-day natural daylight simulation,
|
|
161
|
+
likely a paid/premium feature -- see `mobius.modifiers` docstring for
|
|
162
|
+
details) is a separate, rarer modifier the app also layers in; still
|
|
163
|
+
not implemented -- see `documentation/06-light-schedule.md`.
|
|
164
|
+
- 19 new tests across `tests/test_modifiers.py` and
|
|
165
|
+
`tests/test_light_intensity_branching.py`.
|
|
166
|
+
|
|
167
|
+
## 0.1.2
|
|
168
|
+
|
|
169
|
+
- Added `manufacturer_for_model()` / `MODEL_MANUFACTURER` (ported from
|
|
170
|
+
`M.Model.getManufacturer()`) — surfaces "EcoTech Marine" /
|
|
171
|
+
"AquaIllumination" / "Neptune Systems" / "NYOS" per device model. Now
|
|
172
|
+
included automatically in `get_device_info()`'s `"manufacturer"` field.
|
|
173
|
+
Added while building a Home Assistant integration on top of this library,
|
|
174
|
+
which needs a manufacturer string for its device registry.
|
|
175
|
+
- `get_device_info()`'s `"serial"` field is now decoded as ASCII text
|
|
176
|
+
instead of hex-encoded raw bytes (e.g. `"7V4Z00F143RBED"` instead of
|
|
177
|
+
`"3756345a30304631343352424544"`). Confirmed printable ASCII on every
|
|
178
|
+
device tested, via cross-validation against the same serial parsed from
|
|
179
|
+
BLE advertisement manufacturer data. Falls back to hex for any
|
|
180
|
+
non-printable-ASCII edge case not yet observed. Found while building the
|
|
181
|
+
Home Assistant integration on top of this library -- the hex-encoded
|
|
182
|
+
serial was producing unnecessarily long/ugly entity IDs.
|
|
183
|
+
- `get_pump_telemetry()` now includes a confirmed `"speed_percent"` field
|
|
184
|
+
alongside the existing raw `"speed"`. `MotorSpeed` is confirmed (via the
|
|
185
|
+
decompiled app's own display code) to be a percentage of max pump power
|
|
186
|
+
in tenths of a percent, NOT RPM -- `String.format("%.0f%%",
|
|
187
|
+
Math.abs(currentSpeed / 10.0f))` is literally what the app does to this
|
|
188
|
+
exact value. `speed_percent` is that same `abs(speed)/10` conversion;
|
|
189
|
+
`speed` is still returned raw since its sign encodes rotation direction
|
|
190
|
+
(reverse), which `speed_percent` discards.
|
|
191
|
+
- Fixed a real connection leak in `MobiusDevice.connect()`: if
|
|
192
|
+
`start_notify()` failed after the BLE connection was already
|
|
193
|
+
established (a known common flaky point right after a fresh connection),
|
|
194
|
+
the exception propagated out of `connect()` (`__aenter__`) -- and per the
|
|
195
|
+
async context manager protocol, a raising `__aenter__` means `__aexit__`
|
|
196
|
+
(and therefore `disconnect()`) is never called by the caller's `async
|
|
197
|
+
with` block. The already-open connection was left orphaned on the
|
|
198
|
+
adapter. Since callers like Home Assistant coordinators retry
|
|
199
|
+
periodically, this leak compounded over repeated failures, plausibly
|
|
200
|
+
exhausting a real adapter's connection-slot pool over time even with no
|
|
201
|
+
other integration competing for it and without any range/signal issue.
|
|
202
|
+
`connect()` now cleans up (disconnects) before re-raising if anything
|
|
203
|
+
fails after the connection is established. Added regression tests
|
|
204
|
+
confirming both the cleanup-on-failure and no-spurious-disconnect-on-
|
|
205
|
+
success behavior.
|
|
206
|
+
|
|
207
|
+
## 0.1.0 — initial release
|
|
208
|
+
|
|
209
|
+
- Core FSCI/C2CI wire protocol: framing, CRC16, Get/Set-attribute codec.
|
|
210
|
+
Verified byte-for-byte against real captured packets.
|
|
211
|
+
- Scene control (`start_scene`, `start_feed_mode`, `resume_schedule`).
|
|
212
|
+
- Pump telemetry (speed, estimated GPH) — verified live against real
|
|
213
|
+
VorTech MP40QD pumps.
|
|
214
|
+
- Pump schedule reading (mode + parameters per time block) — verified live.
|
|
215
|
+
- Light schedule reading + client-side interpolation, matching the app's
|
|
216
|
+
own behavior — verified live against real Radion XR15 G6 Pro lights.
|
|
217
|
+
- Device discovery, identity (`get_device_info`), and a unified
|
|
218
|
+
`get_device_summary()` with explicit support tiers
|
|
219
|
+
(light / pump / pump-experimental / unsupported).
|
|
220
|
+
- Advertisement-only discovery: model, serial, and `pan_id` (tank/mesh
|
|
221
|
+
grouping) with no GATT connection required — verified against 4 real
|
|
222
|
+
devices sharing one `pan_id`.
|
|
223
|
+
- `mobius-scan` CLI.
|
|
224
|
+
|
|
225
|
+
### Known gaps
|
|
226
|
+
|
|
227
|
+
- `DoseV1` and `HotSauceV1` primitive types have no parser (see
|
|
228
|
+
`documentation/10-known-gaps-and-open-questions.md`).
|
|
229
|
+
- `CoffeeV1` (NYOS Quantum) pump-primitive support is structurally
|
|
230
|
+
plausible but unverified against real hardware.
|
|
231
|
+
- Thread/CoAP device-to-device relay (used by the official app to reach
|
|
232
|
+
devices without a direct BLE connection) is not implemented — this
|
|
233
|
+
library always connects directly to each device instead.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
|
|
@@ -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'
|
|
@@ -51,6 +51,8 @@ Interface conventions, extended by EcoTech).
|
|
|
51
51
|
| [08-manufacturer-data.md](./08-manufacturer-data.md) | BLE advertisement format (no-connection discovery) |
|
|
52
52
|
| [09-thread-coap-relay.md](./09-thread-coap-relay.md) | How the official app relays to devices without direct BLE (not implemented here) |
|
|
53
53
|
| [10-known-gaps-and-open-questions.md](./10-known-gaps-and-open-questions.md) | Everything not yet confirmed or implemented |
|
|
54
|
+
| [11-device-status-attributes.md](./11-device-status-attributes.md) | Flow range, override mode, battery backup, group/sync, calibration (light-only), maintenance |
|
|
55
|
+
| [12-device-identity-and-address-stability.md](./12-device-identity-and-address-stability.md) | Why device identity is serial-number-based, not address-based |
|
|
54
56
|
|
|
55
57
|
## On the use of AI assistance
|
|
56
58
|
|
|
@@ -87,7 +87,7 @@ against real pumps and lights returned correctly-structured, sane data.
|
|
|
87
87
|
| `ErrorState` | 107 | See [04-device-identity.md](./04-device-identity.md) |
|
|
88
88
|
| `CurrentScene` | 401 | Set to `sceneId(u16 LE) + durationMinutes(u16 LE)` to start a scene |
|
|
89
89
|
| `Schedule1` / `Schedule2` | 500 / 503 | The programmed point-schedule — see 06/07 |
|
|
90
|
-
| `MotorSpeed` | 700 | Live pump speed (int16) |
|
|
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 code, `String.format("%.0f%%", Math.abs(currentSpeed / 10.0f))`. |
|
|
91
91
|
| `SupportedColorChannels` | 901 | Static list of a light's channels |
|
|
92
92
|
|
|
93
93
|
See `src/mobius/constants.py` for the complete implemented list.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Light schedule
|
|
2
|
+
|
|
3
|
+
## There is no "current intensity" attribute
|
|
4
|
+
|
|
5
|
+
This is the single most important, least obvious thing about lights in
|
|
6
|
+
this protocol: **the app does not read live output from a light at all.**
|
|
7
|
+
There's no attribute you can `Get` that tells you what a light is currently
|
|
8
|
+
displaying. Instead, the app fetches the entire programmed point-schedule
|
|
9
|
+
and computes the current intensity **client-side**, by linearly
|
|
10
|
+
interpolating between the two points that bracket the current time, then
|
|
11
|
+
applying a schedule-level master intensity scalar on top (see below), plus
|
|
12
|
+
acclimation-ramp and lunar-cycle modifiers this library still doesn't
|
|
13
|
+
implement.
|
|
14
|
+
|
|
15
|
+
This library's `get_current_light_intensities()` replicates that
|
|
16
|
+
client-side computation. **Correction to an earlier version of this doc**:
|
|
17
|
+
this was previously described as "confirmed correct by validating against
|
|
18
|
+
real fetched schedules" -- that validation only checked the per-channel
|
|
19
|
+
interpolation curve, not the schedule-level scalar below, which was
|
|
20
|
+
missing entirely at the time and silently returned values that didn't
|
|
21
|
+
match the app's real displayed numbers. Caught by direct comparison
|
|
22
|
+
against real app screenshots. Fixed; see below.
|
|
23
|
+
|
|
24
|
+
## Schedule-level master intensity (`Schedule1Intensity`, attribute 511)
|
|
25
|
+
|
|
26
|
+
Confirmed via `WidgetViewCell.java`/`IntensityView.java` in the decompiled
|
|
27
|
+
app: on top of the per-channel interpolated curve, the app applies a
|
|
28
|
+
**separate schedule-level master dimmer** -- `dCurrentIntensity =
|
|
29
|
+
schedule.intensity / 1000.0`, multiplied onto every channel's interpolated
|
|
30
|
+
value. This is genuinely separate from any individual channel's own value,
|
|
31
|
+
including the "Brightness" `VisualID` channel (which is just another
|
|
32
|
+
regular per-channel value some models expose, not this).
|
|
33
|
+
|
|
34
|
+
`get_schedule_intensity()` fetches this (a plain `Get` on
|
|
35
|
+
`Schedule1Intensity`/`Schedule2Intensity`, no special indexing -- a short,
|
|
36
|
+
0-1000, default 500/50% per the app's own initialization code).
|
|
37
|
+
`get_current_light_intensities()` now applies it automatically.
|
|
38
|
+
|
|
39
|
+
**Update**: lunar-phase and acclimation-ramp reductions, which are layered
|
|
40
|
+
on top of this same scalar under specific conditions (night + lunar phases
|
|
41
|
+
enabled; an active, incomplete acclimation period), are now also
|
|
42
|
+
implemented -- see "Lunar phase and acclimation ramp" below. "Insolation"
|
|
43
|
+
(a separate, rarer modifier) is still not.
|
|
44
|
+
|
|
45
|
+
## Wire format
|
|
46
|
+
|
|
47
|
+
### `Schedule1` / `Schedule2` attribute (500 / 503)
|
|
48
|
+
|
|
49
|
+
`Get`-ing this attribute with `index=0, count=0xFFFF` returns every
|
|
50
|
+
programmed point as a separate element in the response (see
|
|
51
|
+
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md) for the
|
|
52
|
+
general Get-response shape).
|
|
53
|
+
|
|
54
|
+
### Point format
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
time (u16 LE, minutes since midnight, 0-1439)
|
|
58
|
+
flags (u8)
|
|
59
|
+
primitiveData (variable)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Flags (confirmed bitmasks from the decompile):
|
|
63
|
+
|
|
64
|
+
| Bit value | Meaning |
|
|
65
|
+
|---|---|
|
|
66
|
+
| 1 | `ACTIVE` — points without this bit set are skipped by this library entirely |
|
|
67
|
+
| 2 | `NIGHT` |
|
|
68
|
+
| 6 | `SUNRISE` (combination flag) |
|
|
69
|
+
| 10 | `SUNSET` (combination flag) |
|
|
70
|
+
|
|
71
|
+
### `LightPrimitive` (the `primitiveData` for `VisualV1` points)
|
|
72
|
+
|
|
73
|
+
Repeating triplets, one per channel the point defines:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
VisualID (u8) + intensity (u16 LE, 0-1000 = 0.0-100.0%)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Confirmed via a full 9-point schedule fetched live from two real XR15
|
|
80
|
+
lights (identical schedules on both — a deliberately symmetric setup, not a
|
|
81
|
+
parsing artifact, confirmed by the two lights having different serial
|
|
82
|
+
numbers).
|
|
83
|
+
|
|
84
|
+
## Interpolation algorithm
|
|
85
|
+
|
|
86
|
+
Ported from `PointSchedule.getIntensitiesAtTime()`:
|
|
87
|
+
|
|
88
|
+
1. Sort points by time.
|
|
89
|
+
2. Find the two points that bracket the target minute-of-day, wrapping
|
|
90
|
+
around midnight (the point after the last one is treated as being on
|
|
91
|
+
"the next day," i.e. its time + 1440).
|
|
92
|
+
3. For each channel present in either bracketing point, linearly
|
|
93
|
+
interpolate: `value(t) = v1 + (v2 - v1) * (t - t1) / (t2 - t1)`.
|
|
94
|
+
4. Channels absent from a point default to `0` (off).
|
|
95
|
+
|
|
96
|
+
Verified against a synthetic two-point schedule (exact endpoint values,
|
|
97
|
+
midpoint blending, and midnight-wraparound blending all behave correctly —
|
|
98
|
+
see `tests/test_light_schedule.py`) and against a real 9-point schedule
|
|
99
|
+
fetched live, which produced a coherent daily light-intensity curve
|
|
100
|
+
(dim moonlight overnight → ramp to full blues/UV midday → wind back down).
|
|
101
|
+
|
|
102
|
+
## `SupportedColorChannels` (attribute 901)
|
|
103
|
+
|
|
104
|
+
A static list of `VisualID` values (channel identities) a given light has —
|
|
105
|
+
this is capability metadata, not live state. Confirmed via `Get` with
|
|
106
|
+
`index=0, count=0xFFFF` against real hardware; both test lights reported
|
|
107
|
+
the same 13-channel set (`Violet, CoolWhite, WarmWhite, Blue, UV, RoyalBlue,
|
|
108
|
+
Green, Red, MoonlightBlue, MoonlightWhite, Brightness, StormProbability,
|
|
109
|
+
CloudProbability`).
|
|
110
|
+
|
|
111
|
+
## Lunar phase and acclimation ramp
|
|
112
|
+
|
|
113
|
+
Both now implemented, on top of the schedule-level master intensity above.
|
|
114
|
+
Ported from `LunarInfo.java`/`AcclimationInfo.java` in the decompiled app.
|
|
115
|
+
|
|
116
|
+
**Lunar phase** (`lunar_percent_reduction(date)`): a simplified moon-phase
|
|
117
|
+
model -- a Julian-Day-based calculation (`julian_day_from_date()`, the
|
|
118
|
+
standard Fliegel-Van Flandern conversion) feeding a 16-entry lookup table
|
|
119
|
+
(`LUNAR_CYCLE_SCALERS`), returning `0.0` at new moon ramping to `1.0` at
|
|
120
|
+
full moon. Verified against real, independently-confirmed 2026 moon phase
|
|
121
|
+
dates (full moon Jan 3 2026, new moon Jan 18 2026) -- the ported algorithm
|
|
122
|
+
matches exactly.
|
|
123
|
+
|
|
124
|
+
This replaces the normal schedule-intensity scalar specifically during the
|
|
125
|
+
**dusk-to-night segment** of the schedule (see `is_night_segment()` below),
|
|
126
|
+
and only when `LunarPhasesEnabled` (attribute 907) is set on the device --
|
|
127
|
+
otherwise that segment just uses a flat `1.0` (full/unreduced) scalar,
|
|
128
|
+
matching the app's own fallback.
|
|
129
|
+
|
|
130
|
+
**`is_night_segment()`**: ported from the boolean half of
|
|
131
|
+
`PointSchedule.getIntensitiesAtTime()`'s return value. This is *not*
|
|
132
|
+
simply "is it night" -- confirmed by reading the Java source directly, it's
|
|
133
|
+
specifically true for the segment whose start point is NOT flagged `NIGHT`
|
|
134
|
+
or `SUNRISE` and whose end point IS flagged `NIGHT` (the dusk-transition
|
|
135
|
+
segment), or when the query time exactly matches a `NIGHT`-flagged point.
|
|
136
|
+
|
|
137
|
+
**Acclimation ramp** (`AcclimationInfo`): a linear intensity ramp from
|
|
138
|
+
`start_intensity` (0-1000 permille) up to full (1000) over `period_days`,
|
|
139
|
+
starting at `start_time` (unix timestamp). Applied as an additional
|
|
140
|
+
multiplier on the schedule-level intensity, only during normal (non-night)
|
|
141
|
+
segments, only while `AcclimationEnabled` is set and the ramp period hasn't
|
|
142
|
+
completed yet.
|
|
143
|
+
|
|
144
|
+
`get_current_light_intensities()` fetches and applies all of this
|
|
145
|
+
automatically -- no separate calls needed for the common case.
|
|
146
|
+
|
|
147
|
+
## Insolation detection (flagged, not implemented)
|
|
148
|
+
|
|
149
|
+
Insolation itself is not implemented (see below), but
|
|
150
|
+
`get_current_light_intensities()` does cheaply detect whether it's active
|
|
151
|
+
and flag it, via `LightIntensityResult` -- the return value is a real
|
|
152
|
+
dict (fully backward compatible with code that indexes/iterates it like a
|
|
153
|
+
plain dict), with an added `.diagnostics` attribute:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
result = await device.get_current_light_intensities()
|
|
157
|
+
if result.diagnostics["insolation_active"]:
|
|
158
|
+
# values were computed from the wrong schedule entirely -- see below
|
|
159
|
+
...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This is a best-effort warning, not a refusal: the values are still
|
|
163
|
+
returned even when Insolation is detected as active, since a possibly-
|
|
164
|
+
wrong-but-present value is often more useful than nothing, and the
|
|
165
|
+
severity depends on how different the real Insolation-derived schedule is
|
|
166
|
+
from the manually-programmed one on a given day.
|
|
167
|
+
|
|
168
|
+
## Hyperdrive (boosted channels above 100%)
|
|
169
|
+
|
|
170
|
+
Some light channels can be driven above their normally-rated power
|
|
171
|
+
(`MaxPower` > `NormalPower` for that channel), allowing programmed
|
|
172
|
+
intensity values above `1000` permille (100%) on the wire, up to `2000`
|
|
173
|
+
(200% of normal). Naively dividing a boosted raw value by 10 to get a
|
|
174
|
+
percentage is wrong once it's above 1000 -- the true percentage depends on
|
|
175
|
+
how much headroom that specific channel actually has.
|
|
176
|
+
|
|
177
|
+
`get_current_light_intensities()` (and `interpolate_light_schedule()`
|
|
178
|
+
underneath it) already handled boosted raw values correctly all along --
|
|
179
|
+
nothing was capped or clamped, so the interpolation itself was never
|
|
180
|
+
wrong. What was missing was converting a boosted raw value to an accurate
|
|
181
|
+
*percentage* for display.
|
|
182
|
+
|
|
183
|
+
`get_current_light_percentages()` is the corrected alternative: it fetches
|
|
184
|
+
`MaxPower`/`NormalPower` (`get_channel_power_info()`, attributes `1504`/
|
|
185
|
+
`1513`) and applies `channel_percent_value()` (`mobius.power`, ported from
|
|
186
|
+
`LightPowerInfo.getPercentValue()`) per channel. For the overwhelming
|
|
187
|
+
common case -- non-hyperdrive channels/devices, i.e. any raw value at or
|
|
188
|
+
below 1000 -- this gives byte-identical results to the old `raw/10`
|
|
189
|
+
shortcut; the extra machinery only changes anything for genuinely boosted
|
|
190
|
+
channels. It also flags which channels are currently boosted via
|
|
191
|
+
`.diagnostics["hyperdrive_channels"]`.
|
|
192
|
+
|
|
193
|
+
This costs extra BLE round trips beyond `get_current_light_intensities()`
|
|
194
|
+
alone (two more `Get`s for `MaxPower`/`NormalPower`). Use
|
|
195
|
+
`get_current_light_intensities()` directly if you don't need hyperdrive
|
|
196
|
+
accuracy or want to minimize round trips.
|
|
197
|
+
|
|
198
|
+
**Not replicated**: `LightPowerInfo`'s `gm8b4()` lookup table -- a
|
|
199
|
+
hardcoded wattage override for a handful of specific AquaIllumination
|
|
200
|
+
Prime/Hydra model + channel combinations. Only relevant to those specific
|
|
201
|
+
models; the general case (including every EcoTech Radion model this
|
|
202
|
+
library has been verified against) doesn't use it.
|
|
203
|
+
|
|
204
|
+
## Not implemented here
|
|
205
|
+
|
|
206
|
+
- "Insolation" -- a real per-calendar-day natural daylight simulation:
|
|
207
|
+
each light stores a 365-entry table (one per day of the year, packed
|
|
208
|
+
into 8 bytes via `InsolationTable1`/`InsolationTable2`), where every
|
|
209
|
+
entry holds sunrise time, sunset time, sun intensity (0-100%), moonrise
|
|
210
|
+
time, moonset time, and moon intensity (0-100%) for that specific
|
|
211
|
+
calendar day. Lets a light automatically track real seasonal daylight
|
|
212
|
+
variation instead of running one fixed schedule year-round. Gated
|
|
213
|
+
behind `AccountSettings.hasPermission()` in the app -- likely a
|
|
214
|
+
paid/premium feature, not merely an obscure one. Not implemented here;
|
|
215
|
+
treated as always-disabled.
|
|
216
|
+
- Writing/programming a schedule (only reading is implemented).
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Device status attributes (pump-specific and generic)
|
|
2
|
+
|
|
3
|
+
Beyond live telemetry (`get_pump_telemetry()`, see
|
|
4
|
+
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md)) and the
|
|
5
|
+
programmed schedule ([07-pump-schedule.md](./07-pump-schedule.md)), pumps
|
|
6
|
+
expose several additional static-capability and configuration attributes.
|
|
7
|
+
This document also covers a few attributes that aren't pump-specific
|
|
8
|
+
(group/sync info, calibration, maintenance tracking), grouped here since
|
|
9
|
+
they're all read via the same small set of `MobiusDevice` methods.
|
|
10
|
+
|
|
11
|
+
## Pump-specific attributes
|
|
12
|
+
|
|
13
|
+
## Flow range (`get_pump_flow_range()`)
|
|
14
|
+
|
|
15
|
+
`MinimumGallonsPerHour`/`MaximumGallonsPerHour` (707/708) — the pump's
|
|
16
|
+
rated flow spec range. This is static capability info (like
|
|
17
|
+
`SupportedColorChannels` for lights), not a live reading. Confirmed via
|
|
18
|
+
`FlowRange.java` in the decompile to **not** apply to `VectraV1` pumps
|
|
19
|
+
specifically (explicitly excluded in the app's own support check).
|
|
20
|
+
|
|
21
|
+
## Override mode (`get_pump_override_mode()`)
|
|
22
|
+
|
|
23
|
+
`PumpOverrideMode` (705) — `None`/`AllOff`/`Manual`. **Confidence note**:
|
|
24
|
+
confirmed in the decompile only as a *write* target (the app sets it to
|
|
25
|
+
`None` to exit override mode after the pump-side calibration flow) --
|
|
26
|
+
never actually read anywhere in the app itself. This library's read
|
|
27
|
+
implementation is a reasonable inference (plain byte enum, matching every
|
|
28
|
+
other attribute of this shape in the protocol), not something directly
|
|
29
|
+
observed being read by the real app.
|
|
30
|
+
|
|
31
|
+
## Battery backup (`get_battery_backup_info()`, `get_boosted_battery_info()`)
|
|
32
|
+
|
|
33
|
+
- `BatteryBackupSpeed` (706, current setting) / `BatteryBackupMaxSpeed`
|
|
34
|
+
(701, ceiling) — confirmed via `BatterySpeed.java`. Same 0-1000-permille
|
|
35
|
+
speed-percentage convention as `MotorSpeed`.
|
|
36
|
+
- `BoostedBatteryPower` (803) / `BoostedBatteryPowerOnTime` (804) /
|
|
37
|
+
`BoostedBatteryPowerOffTime` (805) — confirmed via `BoostedBattery.java`.
|
|
38
|
+
A pulsed battery-backup power mode. Units for the on/off time fields
|
|
39
|
+
aren't confirmed beyond being raw shorts (plausibly seconds, matching
|
|
40
|
+
the pattern of other duration fields elsewhere in the protocol, but not
|
|
41
|
+
independently verified).
|
|
42
|
+
|
|
43
|
+
All four methods fail soft (return `None`) if the underlying attribute(s)
|
|
44
|
+
aren't supported or fail to fetch, rather than raising -- these are all
|
|
45
|
+
genuinely optional/model-dependent.
|
|
46
|
+
|
|
47
|
+
## Generic (not pump-specific)
|
|
48
|
+
|
|
49
|
+
## Group/sync info (`get_group_info()`)
|
|
50
|
+
|
|
51
|
+
`GroupMaster` (900) / `IsGroupMaster` (911). **Confidence note**: neither
|
|
52
|
+
attribute is referenced anywhere in the decompiled app beyond their own
|
|
53
|
+
enum definitions -- genuinely dead/unused in this app version, the same
|
|
54
|
+
situation as `MotorRPM`. `group_master` is returned as raw bytes rather
|
|
55
|
+
than interpreted. **Confirmed against real hardware to be 28 bytes when
|
|
56
|
+
populated** (a real Radion light returned a 28-byte all-zero value; real
|
|
57
|
+
VorTech pumps tested returned nothing for either attribute, i.e.
|
|
58
|
+
unsupported) -- this corrects an earlier guess here that it was probably
|
|
59
|
+
8 bytes by loose analogy to the `Master` field in `Sync`/`EcoSmartBack`
|
|
60
|
+
pump modes (see [07-pump-schedule.md](./07-pump-schedule.md)); that
|
|
61
|
+
analogy was wrong. The all-zero value observed is consistent with "no
|
|
62
|
+
group master set," but the field's actual semantics remain unconfirmed --
|
|
63
|
+
still dead in the app itself. `is_group_master` uses the
|
|
64
|
+
single-byte-boolean convention confirmed consistently across every other
|
|
65
|
+
boolean attribute in this protocol.
|
|
66
|
+
|
|
67
|
+
## Calibration (`get_calibration_info()`) -- a LIGHT feature, not a pump one
|
|
68
|
+
|
|
69
|
+
`IsCalibrated` (1300) / `LastCalibrationTime` (1304) / `MinCalibratedSpeed`
|
|
70
|
+
(1308) / `MaxCalibratedSpeed` (1309).
|
|
71
|
+
|
|
72
|
+
Despite the "Speed"-sounding attribute names (a naming legacy from these
|
|
73
|
+
attributes' shared use across device types in the protocol), **confirmed
|
|
74
|
+
via the decompiled app's own UI gating** that this is surfaced only for
|
|
75
|
+
lighting-category devices:
|
|
76
|
+
|
|
77
|
+
```java
|
|
78
|
+
// DeviceSettingsFragment.java
|
|
79
|
+
if (Feature.checkSupport(Calibration.class, this.device)
|
|
80
|
+
&& this.device.primitive.category() == M.DeviceCategory.Lighting) {
|
|
81
|
+
this.sects.add(4, Section.Calibration);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Tapping it launches `LightCalibrationActivity`, a dedicated LED low-end/
|
|
86
|
+
high-end calibration wizard (`c2development.mobius.lighting.calibration.*`
|
|
87
|
+
in the decompile). There's a *separate*, unrelated pump-side calibration
|
|
88
|
+
flow in the app (`c2development.mobius.flow.calibration.CalibrationActivity`
|
|
89
|
+
-- zero-flow/max-flow calibration, relevant to Nero-class pumps with a
|
|
90
|
+
genuine flow sensor, gated elsewhere by checking the pump isn't in Vectra
|
|
91
|
+
closed-loop mode) -- **not implemented here**, and unrelated to this
|
|
92
|
+
attribute set. It wouldn't apply to VorTech pumps anyway, which use
|
|
93
|
+
magnetic drive with no physical flow sensor to calibrate.
|
|
94
|
+
|
|
95
|
+
Confirmed via real device testing that VorTech pumps specifically do not
|
|
96
|
+
expose Calibration in the app -- expect `get_calibration_info()` to
|
|
97
|
+
return `None` (unsupported) against VorTech and similar pumps, and to
|
|
98
|
+
work against Radion lights.
|
|
99
|
+
|
|
100
|
+
## Maintenance tracking (`get_maintenance_info()`)
|
|
101
|
+
|
|
102
|
+
`RecommendedMaintenanceInterval` (1400) / `MaintenanceTimer` (1401) /
|
|
103
|
+
`LastMaintenanceTime` (1402) / `MaintenanceDue` (1403). **Confidence
|
|
104
|
+
note**: none of these four attributes are referenced anywhere in the
|
|
105
|
+
decompiled app beyond their own enum definitions -- genuinely dead/unused
|
|
106
|
+
in this app version, the same situation as `MotorRPM` and
|
|
107
|
+
`GroupMaster`/`IsGroupMaster`. Because there's zero evidence for the
|
|
108
|
+
actual byte width/encoding of any of these fields, this deliberately
|
|
109
|
+
returns **raw bytes** for each one rather than guessing a specific
|
|
110
|
+
integer width and risking silently misinterpreting real device data. If
|
|
111
|
+
you have hardware that actually populates these, please report back what
|
|
112
|
+
you observe so the real format can be pinned down.
|
|
113
|
+
|
|
114
|
+
## Firmware and hardware info (`get_firmware_versions()`, `get_hardware_info()`)
|
|
115
|
+
|
|
116
|
+
`FirmwareVersion` (attribute 1) and `HardwareRevision` (attribute 2) are
|
|
117
|
+
both sub-indexed -- a `Get` with `index=0, count=0xFFFF` returns one
|
|
118
|
+
element per component the device has, the same "get all elements" pattern
|
|
119
|
+
as `MaxPower`/`NormalPower`/`SupportedColorChannels`. The sub-index is
|
|
120
|
+
`FirmwareType`/`HardwareInfo` respectively (see `mobius.constants`).
|
|
121
|
+
|
|
122
|
+
**Firmware version display labels are fully confirmed against real
|
|
123
|
+
hardware.** `FirmwareType.name(Model)` in the decompiled app maps each
|
|
124
|
+
`FirmwareType` value to the exact label shown in the UI, gated by
|
|
125
|
+
manufacturer. For EcoTech Marine devices specifically (the `isEtm()`
|
|
126
|
+
branch), confirmed to match real output exactly:
|
|
127
|
+
|
|
128
|
+
| `FirmwareType` | Value | Label |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `OS` | 1 | Radio |
|
|
131
|
+
| `Bootloader` | 2 | Radio Bootloader |
|
|
132
|
+
| `MainMicroOS` | 3 | Product OS |
|
|
133
|
+
| `MainMicroBootloader` | 4 | Product Bootloader |
|
|
134
|
+
| `QCA4020Firmware` | 5 | Radio Firmware |
|
|
135
|
+
| `QCA4020FileSystem` | 6 | Filesystem |
|
|
136
|
+
| `QCA4020M4F` | 7 | Radio OS |
|
|
137
|
+
| `QCA4020M0` | 8 | Radio |
|
|
138
|
+
| `QCA4020WLAN` | 9 | WLAN |
|
|
139
|
+
|
|
140
|
+
A real VorTech pump showed exactly Radio/Radio Bootloader/Product
|
|
141
|
+
OS/Product Bootloader (values 1-4); a real Radion light showed exactly
|
|
142
|
+
Product OS/Product Bootloader/Radio Firmware/Filesystem/Radio OS/Radio/
|
|
143
|
+
WLAN (values 3-9, confirming lights use a QCA4020-based radio chip with
|
|
144
|
+
WiFi+BLE, unlike the simpler radio in pumps). `get_firmware_versions()`
|
|
145
|
+
takes an optional `model` parameter to apply these confirmed labels;
|
|
146
|
+
without it (or for non-EcoTech models, whose label mapping in the
|
|
147
|
+
decompile is a different, unverified branch), falls back to the raw
|
|
148
|
+
`FirmwareType` enum name.
|
|
149
|
+
|
|
150
|
+
Each version is a **dot-joined string built directly from the raw
|
|
151
|
+
response bytes** (e.g. `[4, 0, 21]` -> `"4.0.21"`) -- confirmed via
|
|
152
|
+
`FirmwareType.format()` in the decompile, which does exactly that and
|
|
153
|
+
nothing more exotic.
|
|
154
|
+
|
|
155
|
+
`get_hardware_info()` follows the same fetch pattern for
|
|
156
|
+
`HardwareRevision`/`HardwareInfo`, but **no display-formatting convention
|
|
157
|
+
is confirmed** for those fields (`Color`/`Revision`/`ProductType`/
|
|
158
|
+
`RadioType`/`MotorType`/`Segments` read more like small integer/enum
|
|
159
|
+
codes than version numbers) -- returns raw bytes per field rather than
|
|
160
|
+
guessing a format.
|
|
161
|
+
|
|
162
|
+
Both are included automatically in `get_device_summary()`'s
|
|
163
|
+
`"firmware_versions"` field (using the confirmed EcoTech labels, since
|
|
164
|
+
`get_device_summary()` already has the device's `Model` on hand).
|
|
165
|
+
`get_hardware_info()` is not currently wired into the summary -- call it
|
|
166
|
+
directly if you want it.
|