python-mobius 0.5.0__tar.gz → 0.6.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.5.0 → python_mobius-0.6.0}/CHANGELOG.md +43 -13
- {python_mobius-0.5.0 → python_mobius-0.6.0}/PKG-INFO +7 -1
- {python_mobius-0.5.0 → python_mobius-0.6.0}/README.md +6 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/00-overview.md +4 -2
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/02-framing-and-crc.md +1 -1
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/06-light-schedule.md +103 -44
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/08-manufacturer-data.md +12 -9
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/09-thread-coap-relay.md +6 -4
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/10-known-gaps-and-open-questions.md +22 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/11-device-status-attributes.md +3 -3
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/12-device-identity-and-address-stability.md +5 -11
- python_mobius-0.6.0/documentation/13-attribute-dump.md +246 -0
- python_mobius-0.6.0/documentation/14-batched-attribute-reads.md +80 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/pyproject.toml +1 -1
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/__init__.py +14 -5
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/cli.py +164 -8
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/coap.py +18 -14
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/constants.py +186 -65
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/device.py +1185 -77
- python_mobius-0.6.0/src/mobius/device_status.py +751 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/discovery.py +5 -5
- python_mobius-0.6.0/src/mobius/dump.py +378 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/frame.py +98 -2
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/manufacturer.py +6 -5
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/mesh_address.py +2 -2
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/modifiers.py +7 -4
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/power.py +9 -8
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/relay.py +61 -4
- python_mobius-0.6.0/tests/test_attributes_batch.py +122 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_coap.py +7 -7
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_device_status.py +98 -1
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_device_time.py +96 -2
- python_mobius-0.6.0/tests/test_dump.py +290 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_light_intensity_branching.py +83 -11
- python_mobius-0.6.0/tests/test_metadata_batch.py +321 -0
- python_mobius-0.6.0/tests/test_new_diagnostic_attributes.py +694 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_relay.py +103 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_serial_identity.py +6 -5
- python_mobius-0.6.0/tests/test_supported_attributes.py +245 -0
- python_mobius-0.5.0/src/mobius/device_status.py +0 -323
- {python_mobius-0.5.0 → python_mobius-0.6.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/.gitignore +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/LICENSE +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/05-scenes.md +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/crc.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/src/mobius/schedule.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_device_control.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_device_summary.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_discovery_company_ids.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_frame.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_modifiers.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_networked_thread_devices.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_power.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_pump_telemetry.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.5.0 → python_mobius-0.6.0}/tests/test_serial_decoding.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- get_metadata_batch() now falls back to individual reads if the
|
|
6
|
+
batched request itself fails (e.g. no batch support at all) --
|
|
7
|
+
never returns less than those would have. New `used_batch` field,
|
|
8
|
+
`force_individual_reads` param.
|
|
9
|
+
- Added get_metadata_batch() -- one round-trip for AdvancedFeatures/
|
|
10
|
+
CalibrationInfo/hardware/firmware/channels/error state/time,
|
|
11
|
+
confirmed 6-14x faster than individual reads over relay.
|
|
12
|
+
- Wired set_advanced_features() into cli.py -- one flag per field
|
|
13
|
+
(--set-local-control-enabled/--set-auto-dim-timeout/
|
|
14
|
+
--set-max-fan-speed/--set-fan-shutdown-enabled).
|
|
15
|
+
- Added set_advanced_features() -- writes LocalControlEnabled/
|
|
16
|
+
AutoDimTimeout/MaxFanSpeed/FanShutdownEnabled, whichever are given.
|
|
17
|
+
- get_current_light_intensities() now uses get_app_lunar_date(),
|
|
18
|
+
deliberately replicating a confirmed app bug in its lunar
|
|
19
|
+
calculation instead of the physically correct date.
|
|
20
|
+
- Fixed RelayedMobiusDevice missing overrides for
|
|
21
|
+
get_supported_attributes()/get_attributes_batch() -- both bypassed
|
|
22
|
+
the relay path entirely.
|
|
23
|
+
- Fixed --dump-attributes/--set-time-to-now in cli.py ignoring
|
|
24
|
+
--relay-target.
|
|
25
|
+
- Added get_attributes_batch() -- reads multiple attributes in one
|
|
26
|
+
round-trip.
|
|
27
|
+
- Added get_supported_attributes()/dump_attributes()/
|
|
28
|
+
enrich_attribute_dump(), replicating the app's own attribute-dump
|
|
29
|
+
feature. New CLI flags: --dump-attributes/--parse-dump.
|
|
30
|
+
|
|
3
31
|
## 0.5.0
|
|
4
32
|
|
|
5
33
|
- get_pump_telemetry() now returns gph_reliable/minimum_gph/maximum_gph
|
|
@@ -333,8 +361,8 @@ branch has been merged into main as part of this release.
|
|
|
333
361
|
is a bare, meaningless ack the app itself discards, confirmed
|
|
334
362
|
byte-for-byte via real hardware testing showing exactly 1 byte in every
|
|
335
363
|
relayed read's outer response), matched purely by CoAP token
|
|
336
|
-
(confirmed via reverse engineering:
|
|
337
|
-
|
|
364
|
+
(confirmed via reverse engineering: the request's own token must
|
|
365
|
+
match the response's own token), never by the outer FSCI message ID. Added
|
|
338
366
|
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` -- a
|
|
339
367
|
token-keyed pending-response registry, entirely separate from the
|
|
340
368
|
existing message-ID-based `self._pending` mechanism -- wired into
|
|
@@ -452,7 +480,8 @@ branch has been merged into main as part of this release.
|
|
|
452
480
|
(`get_device_summary()`, `get_current_light_intensities()`, etc.) works
|
|
453
481
|
transparently through relay, confirmed with a test that specifically
|
|
454
482
|
exercises an inherited high-level method rather than just the
|
|
455
|
-
overridden primitives. Confirmed
|
|
483
|
+
overridden primitives. Confirmed via reverse engineering the app that its
|
|
484
|
+
own request wrapping builds a
|
|
456
485
|
complete self-contained FSCI frame (byte-for-byte identical in
|
|
457
486
|
structure to this library's own `build_frame()`), resolving an
|
|
458
487
|
ambiguity flagged in an earlier commit -- relaying a request needed no
|
|
@@ -564,10 +593,11 @@ branch has been merged into main as part of this release.
|
|
|
564
593
|
|
|
565
594
|
- **Major fix: device identity is now serial-number-based, not BLE-address-based.**
|
|
566
595
|
Real-world testing surfaced a device whose advertised MAC address changed
|
|
567
|
-
between runs. Investigating the
|
|
596
|
+
between runs. Investigating the app via reverse engineering
|
|
568
597
|
confirmed the official app never relies on address stability at all --
|
|
569
|
-
|
|
570
|
-
|
|
598
|
+
its own equality and hash logic for a discovered device are based purely
|
|
599
|
+
on serial number, and
|
|
600
|
+
its own update logic explicitly replaces the stored BLE address whenever
|
|
571
601
|
a new scan matches an existing device by serial. This library previously
|
|
572
602
|
assumed address stability throughout.
|
|
573
603
|
- New `find_device_by_serial()` and `dedupe_by_serial()`
|
|
@@ -644,8 +674,8 @@ branch has been merged into main as part of this release.
|
|
|
644
674
|
0-100+ percentages instead of raw permille, flagging boosted channels via
|
|
645
675
|
`.diagnostics["hyperdrive_channels"]`. Confirmed byte-identical to the
|
|
646
676
|
old `raw/10` shortcut for the common (non-boosted) case; only changes
|
|
647
|
-
results for genuinely boosted channels.
|
|
648
|
-
specific
|
|
677
|
+
results for genuinely boosted channels. A confirmed Prime/Hydra-
|
|
678
|
+
specific wattage override table is not replicated -- see
|
|
649
679
|
`documentation/06-light-schedule.md`.
|
|
650
680
|
- Also refactored `get_attribute()` into a thin wrapper around a new
|
|
651
681
|
`get_attribute_raw()`, which preserves the device-reported response
|
|
@@ -715,11 +745,11 @@ branch has been merged into main as part of this release.
|
|
|
715
745
|
Home Assistant integration on top of this library -- the hex-encoded
|
|
716
746
|
serial was producing unnecessarily long/ugly entity IDs.
|
|
717
747
|
- `get_pump_telemetry()` now includes a confirmed `"speed_percent"` field
|
|
718
|
-
alongside the existing raw `"speed"`. `MotorSpeed` is confirmed (via
|
|
719
|
-
|
|
720
|
-
in tenths of a percent, NOT RPM --
|
|
721
|
-
|
|
722
|
-
|
|
748
|
+
alongside the existing raw `"speed"`. `MotorSpeed` is confirmed (via
|
|
749
|
+
reverse engineering the app's own display code) to be a percentage of max pump power
|
|
750
|
+
in tenths of a percent, NOT RPM -- the app's own display logic formats
|
|
751
|
+
this same value with the exact same abs-value, tenths-to-percent
|
|
752
|
+
conversion. `speed_percent` is that same `abs(speed)/10` conversion;
|
|
723
753
|
`speed` is still returned raw since its sign encodes rotation direction
|
|
724
754
|
(reverse), which `speed_percent` discards.
|
|
725
755
|
- Fixed a real connection leak in `MobiusDevice.connect()`: if
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -124,6 +124,12 @@ mobius-scan --adapter hci0
|
|
|
124
124
|
- **Reboot a device** (`reboot()`) — a WRITE, matching the app's own
|
|
125
125
|
"Restart" button exactly. Confirmed against real hardware directly
|
|
126
126
|
connected; not yet confirmed via relay.
|
|
127
|
+
- **Dump every attribute a device supports** (`dump_attributes()`),
|
|
128
|
+
matching the app's own Settings → Troubleshoot diagnostic feature's
|
|
129
|
+
data collection — confirmed against real hardware. Output is enriched
|
|
130
|
+
JSON (resolved names, decoded values where known), not the app's own
|
|
131
|
+
plain-text format. See
|
|
132
|
+
[13-attribute-dump.md](./documentation/13-attribute-dump.md).
|
|
127
133
|
- **Low-level protocol access** (`build_frame`, `get_attribute`,
|
|
128
134
|
`set_attribute`, ...) if you want to go beyond what's wrapped in
|
|
129
135
|
`MobiusDevice`.
|
|
@@ -92,6 +92,12 @@ mobius-scan --adapter hci0
|
|
|
92
92
|
- **Reboot a device** (`reboot()`) — a WRITE, matching the app's own
|
|
93
93
|
"Restart" button exactly. Confirmed against real hardware directly
|
|
94
94
|
connected; not yet confirmed via relay.
|
|
95
|
+
- **Dump every attribute a device supports** (`dump_attributes()`),
|
|
96
|
+
matching the app's own Settings → Troubleshoot diagnostic feature's
|
|
97
|
+
data collection — confirmed against real hardware. Output is enriched
|
|
98
|
+
JSON (resolved names, decoded values where known), not the app's own
|
|
99
|
+
plain-text format. See
|
|
100
|
+
[13-attribute-dump.md](./documentation/13-attribute-dump.md).
|
|
95
101
|
- **Low-level protocol access** (`build_frame`, `get_attribute`,
|
|
96
102
|
`set_attribute`, ...) if you want to go beyond what's wrapped in
|
|
97
103
|
`MobiusDevice`.
|
|
@@ -34,7 +34,7 @@ Interface conventions, extended by EcoTech).
|
|
|
34
34
|
and two Radion XR15 G6 Pro lights, all sharing one tank. Every field
|
|
35
35
|
marked "confirmed" in these docs was checked this way — either by
|
|
36
36
|
successfully round-tripping known values, or by cross-validating two
|
|
37
|
-
independent derivations (e.g. a value read via decompiled
|
|
37
|
+
independent derivations (e.g. a value read via a decompiled source
|
|
38
38
|
matching a value read live over BLE).
|
|
39
39
|
|
|
40
40
|
## Document map
|
|
@@ -53,12 +53,14 @@ Interface conventions, extended by EcoTech).
|
|
|
53
53
|
| [10-known-gaps-and-open-questions.md](./10-known-gaps-and-open-questions.md) | Everything not yet confirmed or implemented |
|
|
54
54
|
| [11-device-status-attributes.md](./11-device-status-attributes.md) | Flow range, override mode, battery backup, group/sync, calibration (light-only), maintenance |
|
|
55
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 |
|
|
56
|
+
| [13-attribute-dump.md](./13-attribute-dump.md) | Reading every attribute a device supports, enriched JSON output, the app-compatible text format |
|
|
57
|
+
| [14-batched-attribute-reads.md](./14-batched-attribute-reads.md) | Reading multiple, different attributes in one round-trip |
|
|
56
58
|
|
|
57
59
|
## On the use of AI assistance
|
|
58
60
|
|
|
59
61
|
The decompiled-source analysis, protocol derivation, and Python
|
|
60
62
|
implementation in this project were done with substantial assistance from
|
|
61
|
-
Claude (Anthropic) — reading the decompiled
|
|
63
|
+
Claude (Anthropic) — reading the decompiled app sources, cross-referencing
|
|
62
64
|
them against the public community research above, resolving several
|
|
63
65
|
decompilation artifacts specific to the reverse-engineering process (see
|
|
64
66
|
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md) for a
|
|
@@ -26,7 +26,7 @@ using this library's own encoder, matching the captured bytes exactly (see
|
|
|
26
26
|
|
|
27
27
|
## opGroup values
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The app's own original constants are signed bytes:
|
|
30
30
|
|
|
31
31
|
| Name | Signed | Unsigned (hex) |
|
|
32
32
|
|---|---|---|
|
|
@@ -190,32 +190,65 @@ automatically -- no separate calls needed for the common case.
|
|
|
190
190
|
**`get_lunar_phase_info()`**: reads `LunarPhasesCurrentDay`/
|
|
191
191
|
`LunarPhasesCurrentScalar` directly off the device -- its own
|
|
192
192
|
firmware-computed lunar state, confirmed against real hardware to
|
|
193
|
-
match `LUNAR_CYCLE_SCALERS[current_day]` exactly
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
device
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
match `LUNAR_CYCLE_SCALERS[current_day]` exactly. This is a THIRD,
|
|
194
|
+
independent value from the two below -- see the real-hardware findings
|
|
195
|
+
just below for why all three (this one, the app's own display, and
|
|
196
|
+
what the LEDs physically do) can genuinely differ from each other.
|
|
197
|
+
|
|
198
|
+
**A confirmed bug in the app itself -- and this library deliberately
|
|
199
|
+
replicates it.** The app's own `date`-to-calendar-day conversion
|
|
200
|
+
(confirmed via disassembling the actual raw bytecode with androguard,
|
|
201
|
+
not just the decompile -- see `get_app_lunar_date()`'s own docstring
|
|
202
|
+
in device.py for the full instruction-level trace) applies a timezone
|
|
203
|
+
shift TWICE: once converting `Epoch` to `LocalTime` (the device's own
|
|
204
|
+
job), and again via the phone's own equivalent calendar-day
|
|
205
|
+
conversion, reading the PHONE's own
|
|
206
|
+
timezone off a value that's already been shifted once. When phone and
|
|
207
|
+
device agree on timezone (assumed here, since this library can't know
|
|
208
|
+
the phone's own setting), the net effect is the SAME offset applied
|
|
209
|
+
twice -- not once. For UTC+1, that means the app's own effective
|
|
210
|
+
calendar day rolls over **two hours early**, not one -- confirmed by
|
|
211
|
+
working the exact arithmetic (`2 * LocalTime - Epoch`) and
|
|
212
|
+
independently verified against real captured device data spanning
|
|
213
|
+
both a real summer evening and a forced-clock winter test:
|
|
214
|
+
|
|
215
|
+
- **Summer (UTC+1)**: at 23:25 and again at 23:55 local -- both well
|
|
216
|
+
before that night's true midnight -- the app's own formula computed
|
|
217
|
+
the NEXT calendar day's scalar, while the device's own reported
|
|
218
|
+
`LunarPhasesCurrentDay`/`CurrentScalar` were both still on the
|
|
219
|
+
PREVIOUS day at those exact same moments. The two only agreed again
|
|
220
|
+
once true midnight had actually passed.
|
|
221
|
+
- **Winter (UTC+0)**: the formula and the device's own reported values
|
|
222
|
+
agreed exactly at every point tested -- confirming the shift
|
|
223
|
+
magnitude really does scale with the timezone offset, and vanishes
|
|
224
|
+
entirely at zero.
|
|
225
|
+
|
|
226
|
+
`get_current_light_intensities()` uses `get_app_lunar_date()`
|
|
227
|
+
specifically (not `get_device_local_date()`, which is the physically
|
|
228
|
+
correct one) for this calculation, because this method's own stated
|
|
229
|
+
job is to replicate what the app itself shows -- bugs included, not to
|
|
230
|
+
compute ground truth. A `ha-mobius` user comparing this library's
|
|
231
|
+
output against the official app should see them agree, including
|
|
232
|
+
during the window where the app is (confirmedly) wrong about the
|
|
233
|
+
calendar day.
|
|
234
|
+
|
|
235
|
+
**A separate, ALSO real-hardware-confirmed finding: the device's own
|
|
236
|
+
physical LED output doesn't track EITHER of the above continuously.**
|
|
237
|
+
Three real dumps of a real light, spanning an actual midnight
|
|
238
|
+
rollover, cross-checked against the device's own live `Intensity`
|
|
239
|
+
(1500) readback and direct visual/power observation, showed the
|
|
240
|
+
device locks its own scalar in ONCE -- at the moment the evening's
|
|
241
|
+
ramp into the night segment physically completes -- and holds that
|
|
242
|
+
exact value fixed for the rest of the night, regardless of what
|
|
243
|
+
`LunarPhasesCurrentDay`/`CurrentScalar` do afterward. Neither this
|
|
244
|
+
library's own calculation nor the app's own display attempts to model
|
|
245
|
+
this transition-locked behavior; both compute a value continuously,
|
|
246
|
+
as if the physical output tracked the calendar day in real time. It
|
|
247
|
+
doesn't. This is a third, genuinely different value from the two
|
|
248
|
+
above, not a bug in either of them -- there's no confirmed way to
|
|
249
|
+
predict the device's own physical value from `Epoch`/`LocalTime` alone
|
|
250
|
+
without also knowing when the most recent schedule transition into
|
|
251
|
+
the night segment actually completed.
|
|
219
252
|
|
|
220
253
|
## Insolation detection (flagged, not implemented)
|
|
221
254
|
|
|
@@ -242,22 +275,31 @@ from the manually-programmed one on a given day.
|
|
|
242
275
|
|
|
243
276
|
`.diagnostics` also includes `"is_night_segment"`, `"lunar_enabled"`,
|
|
244
277
|
`"scalar_source"` (one of `"lunar"`/`"night_no_lunar"`/
|
|
245
|
-
`"schedule_intensity"`),
|
|
246
|
-
applied)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
278
|
+
`"schedule_intensity"`), `"scalar"` (the actual final multiplier
|
|
279
|
+
applied), and, specifically for the `"lunar"` case, `"lunar_date"` (the
|
|
280
|
+
actual date `get_app_lunar_date()` computed -- see that section above
|
|
281
|
+
for why this is deliberately the app's own, confirmed-buggy value, not
|
|
282
|
+
the physically correct one) and `"lunar_date_source"` (one of
|
|
283
|
+
`"app_lunar_date"`/`"local_fallback"`) -- useful for telling apart
|
|
284
|
+
which branch/scalar produced a given value, e.g. when a computed value
|
|
285
|
+
doesn't match the app's own displayed one and it isn't obvious whether
|
|
286
|
+
`is_night_segment()` misjudged a schedule's point structure,
|
|
287
|
+
`get_lunar_enabled()` read incorrectly, or the date computation itself
|
|
288
|
+
went wrong:
|
|
250
289
|
|
|
251
290
|
```python
|
|
252
291
|
result = await device.get_current_light_intensities()
|
|
253
292
|
print(result.diagnostics)
|
|
254
293
|
# {'insolation_active': False, 'is_night_segment': True,
|
|
255
|
-
# 'lunar_enabled': True, 'scalar_source': 'lunar', 'scalar': 0.
|
|
294
|
+
# 'lunar_enabled': True, 'scalar_source': 'lunar', 'scalar': 0.6,
|
|
295
|
+
# 'lunar_date': datetime.date(2026, 8, 29), 'lunar_date_source': 'app_lunar_date'}
|
|
256
296
|
```
|
|
257
297
|
|
|
258
298
|
`lunar_enabled` is `None` (not `False`) when `is_night_segment` is
|
|
259
299
|
`False`, since it isn't checked/relevant outside the night segment --
|
|
260
|
-
don't read a `None` here as "lunar phases are off."
|
|
300
|
+
don't read a `None` here as "lunar phases are off." Similarly,
|
|
301
|
+
`lunar_date`/`lunar_date_source` are both `None` whenever
|
|
302
|
+
`scalar_source` isn't `"lunar"`.
|
|
261
303
|
|
|
262
304
|
## Hyperdrive (boosted channels above 100%)
|
|
263
305
|
|
|
@@ -271,7 +313,7 @@ how much headroom that specific channel actually has.
|
|
|
271
313
|
`get_current_light_percentages()` accounts for this: it fetches
|
|
272
314
|
`MaxPower`/`NormalPower` (`get_channel_power_info()`, attributes `1504`/
|
|
273
315
|
`1513`) and applies `channel_percent_value()` (`mobius.power`, ported from
|
|
274
|
-
|
|
316
|
+
the app's own confirmed equivalent logic) per channel. For the common case --
|
|
275
317
|
non-hyperdrive channels/devices, i.e. any raw value at or below 1000 --
|
|
276
318
|
this gives the same results as a simple `raw/10`; the extra machinery
|
|
277
319
|
only changes anything for genuinely boosted channels. It also flags which
|
|
@@ -282,22 +324,39 @@ alone (two more `Get`s for `MaxPower`/`NormalPower`). Use
|
|
|
282
324
|
`get_current_light_intensities()` directly if hyperdrive accuracy isn't
|
|
283
325
|
needed, or to minimize round trips.
|
|
284
326
|
|
|
285
|
-
**Not replicated**:
|
|
327
|
+
**Not replicated**: a lookup table confirmed present in the app -- a
|
|
286
328
|
hardcoded wattage override for a handful of specific AquaIllumination
|
|
287
329
|
Prime/Hydra model + channel combinations. Only relevant to those specific
|
|
288
330
|
models; the general case (including every EcoTech Radion model this
|
|
289
331
|
library has been verified against) doesn't use it.
|
|
290
332
|
|
|
291
|
-
##
|
|
333
|
+
## The insolation modifier itself -- not implemented here
|
|
334
|
+
|
|
335
|
+
Raw reading of the underlying attributes IS implemented (see
|
|
336
|
+
`get_insolation_tables()`/`InsolationTables` below) -- what's NOT
|
|
337
|
+
implemented is the client-side modifier that would actually use this
|
|
338
|
+
table to adjust intensity based on the calendar day, the way
|
|
339
|
+
`mobius.modifiers`'s own lunar/acclimation modifiers already do for
|
|
340
|
+
their own attributes.
|
|
292
341
|
|
|
293
342
|
- "Insolation" -- a real per-calendar-day natural daylight simulation:
|
|
294
|
-
each light stores a 365-entry table (one per day of the year,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
each light stores a 365-entry table (one per day of the year), where
|
|
344
|
+
every entry holds sunrise time, sunset time, sun intensity (0-100%),
|
|
345
|
+
moonrise time, moonset time, and moon intensity (0-100%) for that
|
|
346
|
+
specific calendar day. Confirmed via reverse engineering the app that
|
|
347
|
+
each entry is packed into 8 bytes -- but the actual raw bytes read
|
|
348
|
+
from `InsolationTable1`/`InsolationTable2` (see
|
|
349
|
+
`get_insolation_tables()`/`InsolationTables` in device_status.py) are
|
|
350
|
+
9 bytes per entry on a real light; this discrepancy is unresolved,
|
|
351
|
+
and since every entry captured so far was all-zero (unconfigured),
|
|
352
|
+
there's no way to independently confirm the field-level byte layout
|
|
353
|
+
either way. `get_insolation_tables()` returns raw per-entry bytes
|
|
354
|
+
rather than decoding this structure, for exactly that reason. Lets a
|
|
355
|
+
light automatically track real seasonal daylight
|
|
299
356
|
variation instead of running one fixed schedule year-round. Gated
|
|
300
|
-
behind
|
|
301
|
-
paid/premium feature, not merely an obscure one.
|
|
302
|
-
|
|
357
|
+
behind an account-level permission check in the app -- likely a
|
|
358
|
+
paid/premium feature, not merely an obscure one. The lunar/
|
|
359
|
+
acclimation MODIFIERS in `mobius.modifiers` are implemented and
|
|
360
|
+
confirmed; this separate, rarer modifier is not, and is treated as
|
|
361
|
+
always-disabled by those modifiers.
|
|
303
362
|
- Writing/programming a schedule (only reading is implemented).
|
|
@@ -25,15 +25,17 @@ care which company ID a payload came from.
|
|
|
25
25
|
|
|
26
26
|
## Payload format
|
|
27
27
|
|
|
28
|
-
`bleak` strips the 2-byte company-ID prefix that the
|
|
29
|
-
|
|
28
|
+
`bleak` strips the 2-byte company-ID prefix that the app's own equivalent
|
|
29
|
+
parser expects as part of its input — so this
|
|
30
30
|
library's `parse_manufacturer_data()` takes the already-stripped 23-byte
|
|
31
|
-
`bleak` payload directly, which corresponds to
|
|
31
|
+
`bleak` payload directly, which corresponds to the app's own byte array
|
|
32
|
+
starting at its own index 2.
|
|
32
33
|
|
|
33
|
-
The
|
|
34
|
+
The app has several format branches keyed by total length and a
|
|
34
35
|
"version" byte (the company ID's own low byte -- see "Company ID" above);
|
|
35
36
|
**only the branch actually observed on real hardware is implemented**
|
|
36
|
-
(23-byte `bleak` payload / 25-byte
|
|
37
|
+
(23-byte `bleak` payload / 25-byte length in the app's own byte-array
|
|
38
|
+
numbering) --
|
|
37
39
|
confirmed identical for both company IDs' own "version 1"/"version 2"
|
|
38
40
|
variant of this specific length. Payloads of any other length return
|
|
39
41
|
`None` rather than guessing at an unimplemented branch.
|
|
@@ -79,11 +81,12 @@ payload, i.e. what `parse_manufacturer_data()` actually receives.)
|
|
|
79
81
|
|
|
80
82
|
## "Flags" — deliberately not exposed
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
The byte right after the company ID is the company ID's own *high byte* -- for EcoTech Marine
|
|
83
85
|
(`0x0202`) that's a constant `0x02`; for AquaIllumination (`0x0001`)
|
|
84
|
-
it's a constant `0x00`. The
|
|
85
|
-
different treatment per company ID
|
|
86
|
-
|
|
86
|
+
it's a constant `0x00`. The app gives this byte its own,
|
|
87
|
+
different treatment per company ID, confirmed via reverse engineering it: the
|
|
88
|
+
EcoTech branch just wraps it
|
|
89
|
+
in an opaque `Flags`-style value with no further use shown; the
|
|
87
90
|
AquaIllumination branch reads it as `pendingNotifications = byte > 0`
|
|
88
91
|
(always `False`, since the byte is a constant `0x00`). Either way,
|
|
89
92
|
it's not independent payload data — decoding "flags" here would just
|
|
@@ -50,19 +50,21 @@ alongside the existing Get(`0x17`)/Set(`0x18`) attribute opcodes:
|
|
|
50
50
|
response actually arrives, as a separate, asynchronous message
|
|
51
51
|
("indication"). Matched to its originating request purely by CoAP
|
|
52
52
|
token, confirmed via reverse engineering the app's own exact
|
|
53
|
-
comparison:
|
|
54
|
-
|
|
53
|
+
comparison: the
|
|
54
|
+
request's own token must match the response's own token. The outer FSCI
|
|
55
55
|
message ID is never used for this correlation -- it only has meaning
|
|
56
56
|
for the single BLE hop to the gateway, not across a full Thread mesh
|
|
57
57
|
round-trip.
|
|
58
58
|
|
|
59
|
-
**Request** (`encode_coap_request()`) --
|
|
59
|
+
**Request** (`encode_coap_request()`) -- confirmed via reverse
|
|
60
|
+
engineering the app's own equivalent request-construction logic:
|
|
60
61
|
```
|
|
61
62
|
IPv6(16 bytes, REVERSED) + token(4, LE) + method(2, LE)
|
|
62
63
|
+ payload_length(2, LE) + request_type(1) + payload
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
**Response** (`decode_coap_response()`) --
|
|
66
|
+
**Response** (`decode_coap_response()`) -- confirmed via reverse
|
|
67
|
+
engineering the app's own equivalent response-construction logic:
|
|
66
68
|
```
|
|
67
69
|
IPv6(16 bytes, NOT reversed) + token(4, LE) + response_code(2, LE)
|
|
68
70
|
+ payload_length(2, LE) + request_type(1) + payload
|
{python_mobius-0.5.0 → python_mobius-0.6.0}/documentation/10-known-gaps-and-open-questions.md
RENAMED
|
@@ -92,6 +92,28 @@ included as a starting point.
|
|
|
92
92
|
matching it against known devices in a scan result is left to the
|
|
93
93
|
caller.
|
|
94
94
|
|
|
95
|
+
## Deferred: broader use of batched attribute reads
|
|
96
|
+
|
|
97
|
+
`get_attributes_batch()` (see
|
|
98
|
+
[14-batched-attribute-reads.md](./14-batched-attribute-reads.md)) reads
|
|
99
|
+
multiple, different attributes in one round-trip — confirmed as a real
|
|
100
|
+
capability, currently used only by `dump_attributes()`. `get_device_summary()`
|
|
101
|
+
makes ~15-20 sequential single-attribute-ish sub-calls of its own, several
|
|
102
|
+
of which are simple, few-attribute reads that could be consolidated the
|
|
103
|
+
same way; `ha-mobius`'s own `coordinator.py` has a separate, independent
|
|
104
|
+
~9-10 call sequence per poll that would need its own, similarly-scoped
|
|
105
|
+
pass (it doesn't call `get_device_summary()` at all, so optimizing that
|
|
106
|
+
method alone wouldn't help it).
|
|
107
|
+
|
|
108
|
+
Deliberately deferred rather than attempted as one sweeping change: many
|
|
109
|
+
of those sub-calls aren't simple attribute reads at all (schedule
|
|
110
|
+
reading, model/primitive-dependent telemetry dispatch), and rewriting
|
|
111
|
+
them to fit a shared batch risks regressing already-confirmed, working
|
|
112
|
+
behavior for a broad efficiency gain that hasn't been scoped call-by-call
|
|
113
|
+
yet. Left for a future pass — planned before a 1.0.0 release, not
|
|
114
|
+
abandoned — starting with identifying which specific sub-calls in each
|
|
115
|
+
repo are safely batchable vs. not, rather than assuming all of them are.
|
|
116
|
+
|
|
95
117
|
## Things that are correctness-affecting gotchas, not gaps
|
|
96
118
|
|
|
97
119
|
- **`IntEnum` zero-value truthiness.** `ErrorState.NoError` and
|
|
@@ -233,10 +233,10 @@ interpreted using its own block's starting index (never assuming one
|
|
|
233
233
|
single starting index covers every returned value).
|
|
234
234
|
|
|
235
235
|
**Firmware version display labels are fully confirmed against real
|
|
236
|
-
hardware.**
|
|
236
|
+
hardware.** The app's own confirmed firmware-label lookup maps each
|
|
237
237
|
`FirmwareType` value to the exact label shown in the UI, gated by
|
|
238
|
-
manufacturer. For EcoTech Marine devices specifically
|
|
239
|
-
|
|
238
|
+
manufacturer. For EcoTech Marine devices specifically,
|
|
239
|
+
confirmed to match real output exactly:
|
|
240
240
|
|
|
241
241
|
| `FirmwareType` | Value | Label |
|
|
242
242
|
|---|---|---|
|
|
@@ -11,17 +11,11 @@ identity, never on address stability: its own equality and hash logic
|
|
|
11
11
|
for a discovered device compares serial numbers only, and its own
|
|
12
12
|
"update" logic for a re-seen device explicitly replaces the stored BLE
|
|
13
13
|
address with whatever the device is currently advertising, rather than
|
|
14
|
-
treating a changed address as a different device.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
stored `BluetoothDevice` (which encapsulates the address) with whatever
|
|
20
|
-
came from the new scan result. This is the app's designed architecture
|
|
21
|
-
for handling devices whose BLE address may change over time (plausibly
|
|
22
|
-
BLE resolvable-private-address rotation, though this library doesn't
|
|
23
|
-
need to know the mechanism, only that the address isn't a safe long-term
|
|
24
|
-
identifier).
|
|
14
|
+
treating a changed address as a different device. This is the app's
|
|
15
|
+
designed architecture for handling devices whose BLE address may change
|
|
16
|
+
over time (plausibly BLE resolvable-private-address rotation, though
|
|
17
|
+
this library doesn't need to know the mechanism, only that the address
|
|
18
|
+
isn't a safe long-term identifier).
|
|
25
19
|
|
|
26
20
|
## API
|
|
27
21
|
|