python-mobius 0.4.4__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.4.4 → python_mobius-0.6.0}/CHANGELOG.md +81 -16
- {python_mobius-0.4.4 → python_mobius-0.6.0}/PKG-INFO +43 -8
- {python_mobius-0.4.4 → python_mobius-0.6.0}/README.md +42 -7
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/00-overview.md +4 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/02-framing-and-crc.md +1 -1
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/03-attributes-and-opcodes.md +6 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/04-device-identity.md +1 -1
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/05-scenes.md +2 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/06-light-schedule.md +118 -31
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/07-pump-schedule.md +7 -7
- python_mobius-0.6.0/documentation/08-manufacturer-data.md +95 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/09-thread-coap-relay.md +83 -4
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/10-known-gaps-and-open-questions.md +58 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/11-device-status-attributes.md +106 -5
- {python_mobius-0.4.4 → 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.4.4 → python_mobius-0.6.0}/pyproject.toml +1 -1
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/__init__.py +16 -7
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/cli.py +254 -6
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/coap.py +18 -14
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/constants.py +271 -63
- python_mobius-0.6.0/src/mobius/device.py +3144 -0
- python_mobius-0.6.0/src/mobius/device_status.py +751 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/discovery.py +16 -9
- python_mobius-0.6.0/src/mobius/dump.py +378 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/frame.py +98 -2
- python_mobius-0.6.0/src/mobius/manufacturer.py +73 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/mesh_address.py +2 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/modifiers.py +13 -10
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/power.py +9 -8
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/relay.py +98 -6
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/schedule.py +5 -6
- python_mobius-0.6.0/tests/test_attributes_batch.py +122 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_coap.py +7 -7
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_connection_cleanup.py +61 -2
- python_mobius-0.6.0/tests/test_device_control.py +59 -0
- python_mobius-0.6.0/tests/test_device_status.py +470 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_device_summary.py +47 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_device_time.py +151 -0
- python_mobius-0.6.0/tests/test_discovery_company_ids.py +107 -0
- python_mobius-0.6.0/tests/test_dump.py +290 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_firmware_versions.py +82 -6
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_light_intensity_branching.py +165 -16
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_light_schedule.py +1 -3
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_manufacturer.py +26 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_manufacturer_lookup.py +5 -7
- python_mobius-0.6.0/tests/test_metadata_batch.py +321 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_modifiers.py +1 -2
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_networked_thread_devices.py +13 -16
- python_mobius-0.6.0/tests/test_new_diagnostic_attributes.py +694 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_pump_schedule.py +5 -5
- python_mobius-0.6.0/tests/test_pump_telemetry.py +133 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_relay.py +235 -1
- {python_mobius-0.4.4 → 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.4.4/documentation/08-manufacturer-data.md +0 -67
- python_mobius-0.4.4/src/mobius/device.py +0 -1677
- python_mobius-0.4.4/src/mobius/device_status.py +0 -186
- python_mobius-0.4.4/src/mobius/manufacturer.py +0 -53
- python_mobius-0.4.4/tests/test_device_status.py +0 -167
- {python_mobius-0.4.4 → python_mobius-0.6.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/.gitignore +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/LICENSE +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/crc.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_frame.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_power.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.4.4 → python_mobius-0.6.0}/tests/test_serial_decoding.py +0 -0
|
@@ -1,5 +1,69 @@
|
|
|
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
|
+
|
|
31
|
+
## 0.5.0
|
|
32
|
+
|
|
33
|
+
- get_pump_telemetry() now returns gph_reliable/minimum_gph/maximum_gph
|
|
34
|
+
-- confirmed via the app's own logic that a raw gph reading isn't
|
|
35
|
+
trustworthy without a supported flow range (a real AI Axis 20
|
|
36
|
+
reported ~8x its own rated max flow).
|
|
37
|
+
- Added support for AquaIllumination devices' own BLE company ID
|
|
38
|
+
(0x0001, alongside EcoTech Marine's 0x0202) -- confirmed via a real
|
|
39
|
+
AI Axis 20 capture. BREAKING: MOBIUS_COMPANY_ID renamed to
|
|
40
|
+
MOBIUS_COMPANY_ID_ECOTECH.
|
|
41
|
+
- Added get_lunar_phase_info(): the device's own firmware-computed
|
|
42
|
+
lunar state, confirmed against real hardware, wired into
|
|
43
|
+
--dump-schedule for comparing against the calculated lunar value.
|
|
44
|
+
- Fixed get_lunar_phase_info() silently returning None on a real read
|
|
45
|
+
failure (indistinguishable from "unsupported") -- now only an empty
|
|
46
|
+
response does that; an actual failure propagates.
|
|
47
|
+
- Documented a confirmed bug in the app itself (double timezone
|
|
48
|
+
application) that can make its own displayed lunar value roll over
|
|
49
|
+
to the next day early -- this library's own calculation is correct
|
|
50
|
+
and unaffected.
|
|
51
|
+
- Added get_advanced_features(): VorTech's "Local Control"/"Led Auto
|
|
52
|
+
Dim" and Radion's "Max Fan Speed"/"Fan Shutdown".
|
|
53
|
+
- Added motor_power_watts to get_pump_telemetry() (Vectra/NYOS Quantum).
|
|
54
|
+
- Added get_vectra_info() and get_coffee_info() (Vectra/NYOS Quantum
|
|
55
|
+
settings, no real hardware to verify against).
|
|
56
|
+
- Fixed get_firmware_versions() showing raw internal names instead of
|
|
57
|
+
proper labels for AquaIllumination devices (Orbit/Axis, Prime/Hydra).
|
|
58
|
+
- RelayedMobiusDevice now fails fast on later reads in the same poll
|
|
59
|
+
cycle after one relay timeout, instead of each one separately
|
|
60
|
+
waiting out its own timeout.
|
|
61
|
+
- Added set_time_to_now() -- writes Epoch with group=1, confirmed
|
|
62
|
+
against real hardware to propagate to the rest of a tank's mesh.
|
|
63
|
+
- Added reboot() -- soft-reboots a device (Reset attribute), matching
|
|
64
|
+
the app's own "Restart" button. Confirmed against real hardware
|
|
65
|
+
directly connected.
|
|
66
|
+
|
|
3
67
|
## 0.4.4
|
|
4
68
|
|
|
5
69
|
- Filled in the rest of the device model list (EcoTech Marine,
|
|
@@ -244,9 +308,8 @@
|
|
|
244
308
|
|
|
245
309
|
- **`get_hardware_info()` now decodes `Color`/`ProductType`/`RadioType`/
|
|
246
310
|
`MotorType` into confirmed display labels**, instead of returning raw
|
|
247
|
-
bytes. Each is itself a confirmed enum with confirmed labels
|
|
248
|
-
|
|
249
|
-
decompile) -- added `Color`, `RadioType`, `MotorType`, `ProductType`
|
|
311
|
+
bytes. Each is itself a confirmed enum with confirmed labels --
|
|
312
|
+
added `Color`, `RadioType`, `MotorType`, `ProductType`
|
|
250
313
|
enums plus their `*_LABELS` dicts to `mobius.constants`. An
|
|
251
314
|
unrecognized value falls back to `"Unknown (N)"` rather than raising.
|
|
252
315
|
`Revision`/`Segments` still have no confirmed enum meaning -- returned
|
|
@@ -298,8 +361,8 @@ branch has been merged into main as part of this release.
|
|
|
298
361
|
is a bare, meaningless ack the app itself discards, confirmed
|
|
299
362
|
byte-for-byte via real hardware testing showing exactly 1 byte in every
|
|
300
363
|
relayed read's outer response), matched purely by CoAP token
|
|
301
|
-
(confirmed via reverse engineering:
|
|
302
|
-
|
|
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
|
|
303
366
|
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` -- a
|
|
304
367
|
token-keyed pending-response registry, entirely separate from the
|
|
305
368
|
existing message-ID-based `self._pending` mechanism -- wired into
|
|
@@ -417,7 +480,8 @@ branch has been merged into main as part of this release.
|
|
|
417
480
|
(`get_device_summary()`, `get_current_light_intensities()`, etc.) works
|
|
418
481
|
transparently through relay, confirmed with a test that specifically
|
|
419
482
|
exercises an inherited high-level method rather than just the
|
|
420
|
-
overridden primitives. Confirmed
|
|
483
|
+
overridden primitives. Confirmed via reverse engineering the app that its
|
|
484
|
+
own request wrapping builds a
|
|
421
485
|
complete self-contained FSCI frame (byte-for-byte identical in
|
|
422
486
|
structure to this library's own `build_frame()`), resolving an
|
|
423
487
|
ambiguity flagged in an earlier commit -- relaying a request needed no
|
|
@@ -529,10 +593,11 @@ branch has been merged into main as part of this release.
|
|
|
529
593
|
|
|
530
594
|
- **Major fix: device identity is now serial-number-based, not BLE-address-based.**
|
|
531
595
|
Real-world testing surfaced a device whose advertised MAC address changed
|
|
532
|
-
between runs. Investigating the
|
|
596
|
+
between runs. Investigating the app via reverse engineering
|
|
533
597
|
confirmed the official app never relies on address stability at all --
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
536
601
|
a new scan matches an existing device by serial. This library previously
|
|
537
602
|
assumed address stability throughout.
|
|
538
603
|
- New `find_device_by_serial()` and `dedupe_by_serial()`
|
|
@@ -609,8 +674,8 @@ branch has been merged into main as part of this release.
|
|
|
609
674
|
0-100+ percentages instead of raw permille, flagging boosted channels via
|
|
610
675
|
`.diagnostics["hyperdrive_channels"]`. Confirmed byte-identical to the
|
|
611
676
|
old `raw/10` shortcut for the common (non-boosted) case; only changes
|
|
612
|
-
results for genuinely boosted channels.
|
|
613
|
-
specific
|
|
677
|
+
results for genuinely boosted channels. A confirmed Prime/Hydra-
|
|
678
|
+
specific wattage override table is not replicated -- see
|
|
614
679
|
`documentation/06-light-schedule.md`.
|
|
615
680
|
- Also refactored `get_attribute()` into a thin wrapper around a new
|
|
616
681
|
`get_attribute_raw()`, which preserves the device-reported response
|
|
@@ -680,11 +745,11 @@ branch has been merged into main as part of this release.
|
|
|
680
745
|
Home Assistant integration on top of this library -- the hex-encoded
|
|
681
746
|
serial was producing unnecessarily long/ugly entity IDs.
|
|
682
747
|
- `get_pump_telemetry()` now includes a confirmed `"speed_percent"` field
|
|
683
|
-
alongside the existing raw `"speed"`. `MotorSpeed` is confirmed (via
|
|
684
|
-
|
|
685
|
-
in tenths of a percent, NOT RPM --
|
|
686
|
-
|
|
687
|
-
|
|
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;
|
|
688
753
|
`speed` is still returned raw since its sign encodes rotation direction
|
|
689
754
|
(reverse), which `speed_percent` discards.
|
|
690
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
|
|
@@ -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,34 @@ 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.
|
|
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).
|
|
107
133
|
- **Low-level protocol access** (`build_frame`, `get_attribute`,
|
|
108
134
|
`set_attribute`, ...) if you want to go beyond what's wrapped in
|
|
109
135
|
`MobiusDevice`.
|
|
@@ -113,13 +139,22 @@ mobius-scan --adapter hci0
|
|
|
113
139
|
| PrimitiveType | Support | Notes |
|
|
114
140
|
|---|---|---|
|
|
115
141
|
| `VisualV1` (Radion, Prime, Hydra, etc.) | ✅ Verified | Lights |
|
|
116
|
-
| `VorTechV1`, `
|
|
142
|
+
| `VorTechV1`, `TurtleV1` (AquaIllumination Axis) | ✅ Verified | Pumps -- confirmed against real hardware directly (VorTech MP40QD, AquaIllumination Axis 20) |
|
|
143
|
+
| `PumpV1`, `VectraV1`, `AlpacaV1` (AquaIllumination Orbit) | ✅ Verified | Pumps -- same wire format as the primitives above, not independently confirmed against their own real hardware |
|
|
117
144
|
| `CoffeeV1` (NYOS Quantum) | ⚠️ Experimental | Same wire structure as pumps per the protocol, untested against real hardware |
|
|
118
145
|
| `DoseV1`, `HotSauceV1` | ❌ Unsupported | Different primitive format; identity info only |
|
|
119
146
|
|
|
120
147
|
`MobiusDevice.get_device_summary()` always tells you which tier applies via
|
|
121
148
|
its `"support"` field — see [`documentation/04-device-identity.md`](./documentation/04-device-identity.md).
|
|
122
149
|
|
|
150
|
+
"Verified" above is about core telemetry/schedule parsing. The
|
|
151
|
+
Vectra/NYOS Quantum-specific settings methods
|
|
152
|
+
(`get_vectra_info()`/`get_coffee_info()`) and `motor_power_watts` are
|
|
153
|
+
newer, implemented from the decompiled source alone, and not verified
|
|
154
|
+
against real Vectra or NYOS Quantum hardware regardless of the table
|
|
155
|
+
above — see
|
|
156
|
+
[known gaps](./documentation/10-known-gaps-and-open-questions.md).
|
|
157
|
+
|
|
123
158
|
## Development
|
|
124
159
|
|
|
125
160
|
```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,34 @@ 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.
|
|
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).
|
|
75
101
|
- **Low-level protocol access** (`build_frame`, `get_attribute`,
|
|
76
102
|
`set_attribute`, ...) if you want to go beyond what's wrapped in
|
|
77
103
|
`MobiusDevice`.
|
|
@@ -81,13 +107,22 @@ mobius-scan --adapter hci0
|
|
|
81
107
|
| PrimitiveType | Support | Notes |
|
|
82
108
|
|---|---|---|
|
|
83
109
|
| `VisualV1` (Radion, Prime, Hydra, etc.) | ✅ Verified | Lights |
|
|
84
|
-
| `VorTechV1`, `
|
|
110
|
+
| `VorTechV1`, `TurtleV1` (AquaIllumination Axis) | ✅ Verified | Pumps -- confirmed against real hardware directly (VorTech MP40QD, AquaIllumination Axis 20) |
|
|
111
|
+
| `PumpV1`, `VectraV1`, `AlpacaV1` (AquaIllumination Orbit) | ✅ Verified | Pumps -- same wire format as the primitives above, not independently confirmed against their own real hardware |
|
|
85
112
|
| `CoffeeV1` (NYOS Quantum) | ⚠️ Experimental | Same wire structure as pumps per the protocol, untested against real hardware |
|
|
86
113
|
| `DoseV1`, `HotSauceV1` | ❌ Unsupported | Different primitive format; identity info only |
|
|
87
114
|
|
|
88
115
|
`MobiusDevice.get_device_summary()` always tells you which tier applies via
|
|
89
116
|
its `"support"` field — see [`documentation/04-device-identity.md`](./documentation/04-device-identity.md).
|
|
90
117
|
|
|
118
|
+
"Verified" above is about core telemetry/schedule parsing. The
|
|
119
|
+
Vectra/NYOS Quantum-specific settings methods
|
|
120
|
+
(`get_vectra_info()`/`get_coffee_info()`) and `motor_power_watts` are
|
|
121
|
+
newer, implemented from the decompiled source alone, and not verified
|
|
122
|
+
against real Vectra or NYOS Quantum hardware regardless of the table
|
|
123
|
+
above — see
|
|
124
|
+
[known gaps](./documentation/10-known-gaps-and-open-questions.md).
|
|
125
|
+
|
|
91
126
|
## Development
|
|
92
127
|
|
|
93
128
|
```bash
|
|
@@ -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
|
|---|---|---|
|
|
@@ -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,69 @@ 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. 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.
|
|
252
|
+
|
|
192
253
|
## Insolation detection (flagged, not implemented)
|
|
193
254
|
|
|
194
255
|
Insolation itself is not implemented (see below), but
|
|
@@ -214,22 +275,31 @@ from the manually-programmed one on a given day.
|
|
|
214
275
|
|
|
215
276
|
`.diagnostics` also includes `"is_night_segment"`, `"lunar_enabled"`,
|
|
216
277
|
`"scalar_source"` (one of `"lunar"`/`"night_no_lunar"`/
|
|
217
|
-
`"schedule_intensity"`),
|
|
218
|
-
applied)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
222
289
|
|
|
223
290
|
```python
|
|
224
291
|
result = await device.get_current_light_intensities()
|
|
225
292
|
print(result.diagnostics)
|
|
226
293
|
# {'insolation_active': False, 'is_night_segment': True,
|
|
227
|
-
# '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'}
|
|
228
296
|
```
|
|
229
297
|
|
|
230
298
|
`lunar_enabled` is `None` (not `False`) when `is_night_segment` is
|
|
231
299
|
`False`, since it isn't checked/relevant outside the night segment --
|
|
232
|
-
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"`.
|
|
233
303
|
|
|
234
304
|
## Hyperdrive (boosted channels above 100%)
|
|
235
305
|
|
|
@@ -243,7 +313,7 @@ how much headroom that specific channel actually has.
|
|
|
243
313
|
`get_current_light_percentages()` accounts for this: it fetches
|
|
244
314
|
`MaxPower`/`NormalPower` (`get_channel_power_info()`, attributes `1504`/
|
|
245
315
|
`1513`) and applies `channel_percent_value()` (`mobius.power`, ported from
|
|
246
|
-
|
|
316
|
+
the app's own confirmed equivalent logic) per channel. For the common case --
|
|
247
317
|
non-hyperdrive channels/devices, i.e. any raw value at or below 1000 --
|
|
248
318
|
this gives the same results as a simple `raw/10`; the extra machinery
|
|
249
319
|
only changes anything for genuinely boosted channels. It also flags which
|
|
@@ -254,22 +324,39 @@ alone (two more `Get`s for `MaxPower`/`NormalPower`). Use
|
|
|
254
324
|
`get_current_light_intensities()` directly if hyperdrive accuracy isn't
|
|
255
325
|
needed, or to minimize round trips.
|
|
256
326
|
|
|
257
|
-
**Not replicated**:
|
|
327
|
+
**Not replicated**: a lookup table confirmed present in the app -- a
|
|
258
328
|
hardcoded wattage override for a handful of specific AquaIllumination
|
|
259
329
|
Prime/Hydra model + channel combinations. Only relevant to those specific
|
|
260
330
|
models; the general case (including every EcoTech Radion model this
|
|
261
331
|
library has been verified against) doesn't use it.
|
|
262
332
|
|
|
263
|
-
##
|
|
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.
|
|
264
341
|
|
|
265
342
|
- "Insolation" -- a real per-calendar-day natural daylight simulation:
|
|
266
|
-
each light stores a 365-entry table (one per day of the year,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
356
|
variation instead of running one fixed schedule year-round. Gated
|
|
272
|
-
behind
|
|
273
|
-
paid/premium feature, not merely an obscure one.
|
|
274
|
-
|
|
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.
|
|
275
362
|
- Writing/programming a schedule (only reading is implemented).
|