python-mobius 0.7.2__tar.gz → 0.8.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.7.2 → python_mobius-0.8.0}/CHANGELOG.md +28 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/PKG-INFO +14 -2
- {python_mobius-0.7.2 → python_mobius-0.8.0}/README.md +13 -1
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/00-overview.md +2 -2
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/03-attributes-and-opcodes.md +2 -1
- python_mobius-0.8.0/documentation/05-scenes.md +137 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/06-light-schedule.md +92 -0
- python_mobius-0.8.0/documentation/07-pump-schedule.md +185 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/09-thread-coap-relay.md +15 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/10-known-gaps-and-open-questions.md +40 -21
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/11-device-status-attributes.md +14 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/13-attribute-dump.md +1 -1
- python_mobius-0.8.0/documentation/14-batched-attribute-reads.md +112 -0
- python_mobius-0.8.0/documentation/15-mob-template-format.md +107 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/pyproject.toml +1 -1
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/__init__.py +22 -5
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/cli.py +541 -16
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/coap.py +35 -36
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/constants.py +142 -81
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/crc.py +3 -3
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/device.py +839 -602
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/device_status.py +146 -146
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/discovery.py +12 -13
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/dump.py +35 -36
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/frame.py +14 -14
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/manufacturer.py +8 -8
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/mesh_address.py +11 -11
- python_mobius-0.8.0/src/mobius/mob.py +188 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/modifiers.py +5 -16
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/power.py +7 -7
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/pump_status.py +6 -6
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/relay.py +67 -22
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/schedule.py +164 -16
- python_mobius-0.8.0/tests/test_cli_group_targets.py +296 -0
- python_mobius-0.8.0/tests/test_cli_mob_file.py +192 -0
- python_mobius-0.8.0/tests/test_cli_schedule_file.py +274 -0
- python_mobius-0.8.0/tests/test_device_control.py +165 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_device_summary.py +84 -0
- python_mobius-0.8.0/tests/test_extra_physical_values.py +79 -0
- python_mobius-0.8.0/tests/test_fan_thresholds.py +93 -0
- python_mobius-0.8.0/tests/test_group_bitmap.py +145 -0
- python_mobius-0.8.0/tests/test_light_schedule.py +482 -0
- python_mobius-0.8.0/tests/test_mob.py +283 -0
- python_mobius-0.8.0/tests/test_pump_schedule.py +472 -0
- python_mobius-0.8.0/tests/test_pump_supported_modes.py +78 -0
- python_mobius-0.8.0/tests/test_pump_telemetry.py +264 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_relay.py +83 -0
- python_mobius-0.8.0/tests/test_schedule_file_format.py +165 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_schedule_intensity_scalar.py +82 -0
- python_mobius-0.7.2/documentation/05-scenes.md +0 -55
- python_mobius-0.7.2/documentation/07-pump-schedule.md +0 -120
- python_mobius-0.7.2/documentation/14-batched-attribute-reads.md +0 -80
- python_mobius-0.7.2/tests/test_device_control.py +0 -59
- python_mobius-0.7.2/tests/test_light_schedule.py +0 -142
- python_mobius-0.7.2/tests/test_pump_schedule.py +0 -148
- python_mobius-0.7.2/tests/test_pump_telemetry.py +0 -133
- {python_mobius-0.7.2 → python_mobius-0.8.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/.gitignore +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/LICENSE +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/src/mobius/scenes.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_attributes_batch.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_coap.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_device_status.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_device_time.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_discovery_company_ids.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_dump.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_frame.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_full_poll_batch.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_light_intensity_branching.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_light_poll_batch.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_metadata_batch.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_modifiers.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_networked_thread_devices.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_new_diagnostic_attributes.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_power.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_scenes.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_serial_identity.py +0 -0
- {python_mobius-0.7.2 → python_mobius-0.8.0}/tests/test_supported_attributes.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- Added `supported_pump_modes(primitive_type, closed_loop=None)`: the
|
|
6
|
+
`PumpMode` values a given pump actually supports. See
|
|
7
|
+
documentation/07-pump-schedule.md's own "Which modes a given pump
|
|
8
|
+
actually supports" section.
|
|
9
|
+
- Added schedule write support: set_light_schedule()/set_pump_schedule(),
|
|
10
|
+
GroupBitmap (group_mask), and --dump-schedule-file/--load-schedule-file
|
|
11
|
+
CLI flags.
|
|
12
|
+
- Added support for .mob (Template) files -- export/import for both
|
|
13
|
+
light and pump schedules, plus --dump-mob-file/--load-mob-file CLI
|
|
14
|
+
flags.
|
|
15
|
+
- --load-schedule-file/--load-mob-file now write to every device in a
|
|
16
|
+
light's schedule group, not just the one named, and check the whole
|
|
17
|
+
group's smallest schedule capacity up front so a too-large schedule
|
|
18
|
+
aborts cleanly instead of partially applying.
|
|
19
|
+
- get_pump_telemetry() now also includes every other sensor value a
|
|
20
|
+
pump reports supporting (motor RPM, temperatures, battery/input
|
|
21
|
+
voltage, etc.) under a new extra_physical_values key, alongside the
|
|
22
|
+
existing gph/motor_power_watts.
|
|
23
|
+
- Added get_extra_physical_values() -- the same extra sensor values as
|
|
24
|
+
above, for callers with no get_pump_telemetry()-equivalent method of
|
|
25
|
+
their own. get_device_summary() uses this for lights specifically
|
|
26
|
+
(ClusterTemperature/DriverTemperature/etc.), which previously had no
|
|
27
|
+
way to surface this data at all.
|
|
28
|
+
- Added get_fan_thresholds() -- FanOnTemperature/FanOffTemperature,
|
|
29
|
+
shown unconditionally in get_device_summary().
|
|
30
|
+
|
|
3
31
|
## 0.7.2
|
|
4
32
|
|
|
5
33
|
- get_full_poll_batch() now includes scene data (ConfiguredScenes/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -102,7 +102,13 @@ mobius-scan --adapter hci0
|
|
|
102
102
|
- **Discover devices** and group them by tank/mesh (`pan_id`), reading
|
|
103
103
|
model/serial straight from BLE advertisements — no connection required.
|
|
104
104
|
- **Read pump telemetry**: current speed, estimated flow (GPH), operation
|
|
105
|
-
state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts
|
|
105
|
+
state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts —
|
|
106
|
+
plus every OTHER live sensor value (motor RPM, temperatures, battery/input
|
|
107
|
+
voltage, etc.) a specific pump reports supporting, merged into the same
|
|
108
|
+
result under `extra_physical_values` rather than a separate call. Lights
|
|
109
|
+
get the same extra sensor values too (`get_extra_physical_values()`) —
|
|
110
|
+
a light has no telemetry method of its own to merge them into, but
|
|
111
|
+
`get_device_summary()` still surfaces them the same way.
|
|
106
112
|
- **Read pump schedules**: which mode (constant speed, tidal swell, pulse,
|
|
107
113
|
etc.) is active at any given time, exactly as programmed.
|
|
108
114
|
- **Read light schedules**: per-channel intensity at any given time,
|
|
@@ -127,6 +133,12 @@ mobius-scan --adapter hci0
|
|
|
127
133
|
(`get_vectra_info()`/`get_coffee_info()` — no real hardware to verify
|
|
128
134
|
either against, see
|
|
129
135
|
[known gaps](./documentation/10-known-gaps-and-open-questions.md)).
|
|
136
|
+
- **Read a device's own fan protection thresholds** (`get_fan_thresholds()`)
|
|
137
|
+
— FanOnTemperature/FanOffTemperature, if supported at all; confirmed
|
|
138
|
+
against real hardware that neither a Radion light nor a VorTech pump
|
|
139
|
+
reports supporting either one, and — forcing a direct read anyway —
|
|
140
|
+
that the device actively rejects it (`FsciStatus.UnsupportedAttribute`),
|
|
141
|
+
not merely omitting it while still functioning underneath.
|
|
130
142
|
- **Fix a desynced device clock** (`set_time_to_now()`) — a WRITE.
|
|
131
143
|
Writing to one device appears to propagate to the rest of its Thread
|
|
132
144
|
mesh too, confirmed against real hardware — see
|
|
@@ -70,7 +70,13 @@ mobius-scan --adapter hci0
|
|
|
70
70
|
- **Discover devices** and group them by tank/mesh (`pan_id`), reading
|
|
71
71
|
model/serial straight from BLE advertisements — no connection required.
|
|
72
72
|
- **Read pump telemetry**: current speed, estimated flow (GPH), operation
|
|
73
|
-
state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts
|
|
73
|
+
state, error state, and (Vectra/NYOS Quantum pumps) motor power in watts —
|
|
74
|
+
plus every OTHER live sensor value (motor RPM, temperatures, battery/input
|
|
75
|
+
voltage, etc.) a specific pump reports supporting, merged into the same
|
|
76
|
+
result under `extra_physical_values` rather than a separate call. Lights
|
|
77
|
+
get the same extra sensor values too (`get_extra_physical_values()`) —
|
|
78
|
+
a light has no telemetry method of its own to merge them into, but
|
|
79
|
+
`get_device_summary()` still surfaces them the same way.
|
|
74
80
|
- **Read pump schedules**: which mode (constant speed, tidal swell, pulse,
|
|
75
81
|
etc.) is active at any given time, exactly as programmed.
|
|
76
82
|
- **Read light schedules**: per-channel intensity at any given time,
|
|
@@ -95,6 +101,12 @@ mobius-scan --adapter hci0
|
|
|
95
101
|
(`get_vectra_info()`/`get_coffee_info()` — no real hardware to verify
|
|
96
102
|
either against, see
|
|
97
103
|
[known gaps](./documentation/10-known-gaps-and-open-questions.md)).
|
|
104
|
+
- **Read a device's own fan protection thresholds** (`get_fan_thresholds()`)
|
|
105
|
+
— FanOnTemperature/FanOffTemperature, if supported at all; confirmed
|
|
106
|
+
against real hardware that neither a Radion light nor a VorTech pump
|
|
107
|
+
reports supporting either one, and — forcing a direct read anyway —
|
|
108
|
+
that the device actively rejects it (`FsciStatus.UnsupportedAttribute`),
|
|
109
|
+
not merely omitting it while still functioning underneath.
|
|
98
110
|
- **Fix a desynced device clock** (`set_time_to_now()`) — a WRITE.
|
|
99
111
|
Writing to one device appears to propagate to the rest of its Thread
|
|
100
112
|
mesh too, confirmed against real hardware — see
|
|
@@ -45,7 +45,7 @@ Interface conventions, extended by EcoTech).
|
|
|
45
45
|
| [02-framing-and-crc.md](./02-framing-and-crc.md) | Wire frame layout, CRC16 algorithm |
|
|
46
46
|
| [03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md) | Get/Set-attribute opcodes, payload format, the `C2Attribute` table |
|
|
47
47
|
| [04-device-identity.md](./04-device-identity.md) | `PrimitiveType`, `Model`, `ErrorState`, support tiers |
|
|
48
|
-
| [05-scenes.md](./05-scenes.md) | Scene control (
|
|
48
|
+
| [05-scenes.md](./05-scenes.md) | Scene control (reading configured scenes, activating one, mesh-wide broadcast, resuming the schedule) |
|
|
49
49
|
| [06-light-schedule.md](./06-light-schedule.md) | Light point-schedule format + client-side interpolation |
|
|
50
50
|
| [07-pump-schedule.md](./07-pump-schedule.md) | Pump primitive format, mode/parameter tables, block lookup |
|
|
51
51
|
| [08-manufacturer-data.md](./08-manufacturer-data.md) | BLE advertisement format (no-connection discovery) |
|
|
@@ -54,7 +54,7 @@ Interface conventions, extended by EcoTech).
|
|
|
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
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 |
|
|
57
|
+
| [14-batched-attribute-reads.md](./14-batched-attribute-reads.md) | Reading multiple, different attributes in one round-trip, and the combined-poll methods built on top |
|
|
58
58
|
|
|
59
59
|
## On the use of AI assistance
|
|
60
60
|
|
|
@@ -85,7 +85,8 @@ against real pumps and lights returned correctly-structured, sane data.
|
|
|
85
85
|
| `MACAddress` | 103 | Observed unpopulated (empty response) on all devices tested |
|
|
86
86
|
| `OperationState` | 104 | `OOB`/`LiveDemo`/`Scene`/`Schedule` |
|
|
87
87
|
| `ErrorState` | 107 | See [04-device-identity.md](./04-device-identity.md) |
|
|
88
|
-
| `CurrentScene` | 401 | Set to `sceneId(u16 LE) +
|
|
88
|
+
| `CurrentScene` | 401 | Set to `sceneId(u16 LE) + durationSeconds(u16 LE)` to start a scene -- see [05-scenes.md](./05-scenes.md) |
|
|
89
|
+
| `ConfiguredScenes` | 400 | Every scene this device has programmed, one per slot -- see [05-scenes.md](./05-scenes.md) |
|
|
89
90
|
| `Schedule1` / `Schedule2` | 500 / 503 | The programmed point-schedule — see 06/07 |
|
|
90
91
|
| `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
92
|
| `LocalControlEnabled` / `AutoDimTimeout` / `MaxFanSpeed` / `FanShutdownEnabled` | 300 / 301 / 910 / 1203 | VorTech/Radion advanced feature settings — see 11 |
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Scenes
|
|
2
|
+
|
|
3
|
+
Scenes are device-side presets (feed mode, all-off, disco, a user-created
|
|
4
|
+
custom one, etc.) that can be activated on demand, separate from the
|
|
5
|
+
normal time-based schedule. A scene is genuinely tank-wide, not a
|
|
6
|
+
per-device concept — creating one (not implemented by this library; see
|
|
7
|
+
below) writes a matching entry, with the same `id`/`name`/`timeout`, to
|
|
8
|
+
every device in the tank that supports scenes and has a free slot.
|
|
9
|
+
|
|
10
|
+
## `SceneID`
|
|
11
|
+
|
|
12
|
+
Confirmed literal values — the ten built-in presets. A user-created
|
|
13
|
+
custom scene's own `id` is a device-assigned value outside this range
|
|
14
|
+
(see "Configured scenes" below), not one of these:
|
|
15
|
+
|
|
16
|
+
| Value | Name |
|
|
17
|
+
|---|---|
|
|
18
|
+
| 0 | `EmptyScene` |
|
|
19
|
+
| 1 | `FeedMode` |
|
|
20
|
+
| 2 | `BatteryBackup` |
|
|
21
|
+
| 3 | `AllOff` |
|
|
22
|
+
| 4 | `ColorCycle` |
|
|
23
|
+
| 5 | `Disco` |
|
|
24
|
+
| 6 | `Thunderstorm` |
|
|
25
|
+
| 7 | `CloudCover` |
|
|
26
|
+
| 8 | `AllOn` |
|
|
27
|
+
| 9 | `All50` |
|
|
28
|
+
|
|
29
|
+
## Configured scenes
|
|
30
|
+
|
|
31
|
+
`C2Attribute.ConfiguredScenes` (400) holds every scene this device has
|
|
32
|
+
programmed, one per slot, read via the same "get all elements" pattern
|
|
33
|
+
used for the point schedule (`index=0, count=0xFFFF`) — the number of
|
|
34
|
+
slots a device actually has is device-reported, not a fixed constant,
|
|
35
|
+
and can genuinely differ by device type. Each element is:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
id (u16 LE) + timeout (u16 LE) + name (16 bytes, null-padded) + primitiveData
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`primitiveData` is exactly the same payload a schedule point's own
|
|
42
|
+
primitive would be — a light's own per-channel intensities, or a pump's
|
|
43
|
+
own mode + parameters (see
|
|
44
|
+
[06-light-schedule.md](./06-light-schedule.md)/
|
|
45
|
+
[07-pump-schedule.md](./07-pump-schedule.md)) — just without the
|
|
46
|
+
schedule-point header (time/flags) wrapping it, since a scene isn't
|
|
47
|
+
tied to a time of day. A custom scene's own `id` is a randomly-chosen
|
|
48
|
+
value confirmed unique across the whole tank (not just this one
|
|
49
|
+
device); a genuinely empty/unused slot has `id=EmptyScene` and an empty
|
|
50
|
+
`name`.
|
|
51
|
+
|
|
52
|
+
`get_configured_scenes()` reads and decodes this into a `Scene` per
|
|
53
|
+
slot (`index`/`id`/`scene_type`/`name`/`timeout`/`light`/`pump`) —
|
|
54
|
+
`scene_type` is `None` whenever `id` doesn't match one of the ten
|
|
55
|
+
built-in presets, i.e. exactly the custom-scene case.
|
|
56
|
+
|
|
57
|
+
## The currently active scene
|
|
58
|
+
|
|
59
|
+
`C2Attribute.CurrentScene` (401) reports whichever scene is currently
|
|
60
|
+
active:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
id (u16 LE) + durationSeconds (u16 LE)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`get_current_scene()` reads and decodes this into an `ActiveScene`
|
|
67
|
+
(`id`/`scene_type`/`duration_seconds`) — `scene_type` is `None` for a
|
|
68
|
+
custom scene's own id here too, the same as above.
|
|
69
|
+
|
|
70
|
+
## Starting a scene
|
|
71
|
+
|
|
72
|
+
`start_scene(scene_id, duration_seconds=0)` sets `CurrentScene` to:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
sceneId (u16 LE) + durationSeconds (u16 LE)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`duration_seconds=0` lets the device use the scene's own configured
|
|
79
|
+
timeout rather than overriding it. The unit is confirmed to be
|
|
80
|
+
**seconds**, not minutes — traced directly through the app's own
|
|
81
|
+
duration-remaining calculation, which multiplies this same value by
|
|
82
|
+
1000 to build a millisecond timestamp offset.
|
|
83
|
+
|
|
84
|
+
**Confirmed live and via byte-for-byte packet reconstruction**: building a
|
|
85
|
+
"start feed mode" frame with this library's own encoder reproduces a real
|
|
86
|
+
packet captured from the community reverse-engineering thread exactly —
|
|
87
|
+
see `tests/test_frame.py::test_build_frame_reproduces_captured_bytes_exactly`.
|
|
88
|
+
|
|
89
|
+
### Broadcasting to the whole mesh
|
|
90
|
+
|
|
91
|
+
`start_scene()`/`start_feed_mode()` default to `broadcast=True`, which
|
|
92
|
+
sets the frame's own reserved-byte `group` field to 1 (see
|
|
93
|
+
[02-framing-and-crc.md](./02-framing-and-crc.md)) — a single write to
|
|
94
|
+
one device then propagates to the rest of its Thread mesh, the exact
|
|
95
|
+
same mechanism confirmed against real hardware for `set_time_to_now()`'s
|
|
96
|
+
own `Epoch` write (see
|
|
97
|
+
[09-thread-coap-relay.md](./09-thread-coap-relay.md)). Applying that
|
|
98
|
+
same mechanism to `CurrentScene` specifically hasn't been independently
|
|
99
|
+
verified against real hardware the same way — it's the app's own code
|
|
100
|
+
path for scene activation, not yet its own separate confirmation.
|
|
101
|
+
`group=1` only ever reaches devices on that same mesh; a Thread network
|
|
102
|
+
is its own isolated network (its own `pan_id`), so this cannot cross
|
|
103
|
+
into a different tank's own, separate mesh even within physical radio
|
|
104
|
+
range. Pass `broadcast=False` to write to only the one device instead.
|
|
105
|
+
|
|
106
|
+
## Returning to the normal schedule
|
|
107
|
+
|
|
108
|
+
`resume_schedule()` sets `C2Attribute.OperationState` (104) to
|
|
109
|
+
`OperationState.Schedule` (3). This is what "cancel the current scene"
|
|
110
|
+
actually means at the protocol level — there's no separate "cancel"
|
|
111
|
+
opcode, just setting operation state back to schedule-driven. Unlike
|
|
112
|
+
scene activation, this write has no confirmed mesh-propagation evidence
|
|
113
|
+
of its own — call it per-device if you need it to apply tank-wide.
|
|
114
|
+
|
|
115
|
+
## `OperationState`
|
|
116
|
+
|
|
117
|
+
Confirmed literal values:
|
|
118
|
+
|
|
119
|
+
| Value | Name |
|
|
120
|
+
|---|---|
|
|
121
|
+
| 0 | `OOB` (out-of-box, unconfigured) |
|
|
122
|
+
| 1 | `LiveDemo` |
|
|
123
|
+
| 2 | `Scene` |
|
|
124
|
+
| 3 | `Schedule` |
|
|
125
|
+
|
|
126
|
+
## Not implemented: creating/editing a scene
|
|
127
|
+
|
|
128
|
+
Writing a new or modified `Scene` entry (rather than just activating an
|
|
129
|
+
existing one) is not implemented by this library. The wire format above
|
|
130
|
+
is fully confirmed and sufficient to build one, but doing so correctly
|
|
131
|
+
means writing it to every scene-capable device in the tank individually
|
|
132
|
+
(no broadcast shortcut exists for this — `ConfiguredScenes` is written
|
|
133
|
+
per-device, per-slot, not as a single group write), choosing a slot that
|
|
134
|
+
isn't already in use on each one, and — for a genuinely custom scene —
|
|
135
|
+
generating an `id` confirmed unique across the whole tank first. See
|
|
136
|
+
[10-known-gaps-and-open-questions.md](./10-known-gaps-and-open-questions.md).
|
|
137
|
+
|
|
@@ -29,6 +29,11 @@ regular per-channel value some models expose, not this).
|
|
|
29
29
|
0-1000, default 500/50% per the app's own initialization code).
|
|
30
30
|
`get_current_light_intensities()` applies it automatically.
|
|
31
31
|
|
|
32
|
+
`set_schedule_intensity()` writes it back -- a plain `Set` mirroring the
|
|
33
|
+
same encoding, taking a 0.0-1.0 fraction (matching the getter's own
|
|
34
|
+
return range) and raising `ValueError` before writing anything if it's
|
|
35
|
+
out of that range.
|
|
36
|
+
|
|
32
37
|
Lunar-phase and acclimation-ramp reductions layer on top of this same
|
|
33
38
|
scalar under specific conditions (night + lunar phases enabled; an
|
|
34
39
|
active, incomplete acclimation period) -- see "Lunar phase and
|
|
@@ -72,6 +77,75 @@ VisualID (u8) + intensity (u16 LE, 0-1000 = 0.0-100.0%)
|
|
|
72
77
|
Confirmed against a full 9-point schedule fetched live from real XR15
|
|
73
78
|
lights.
|
|
74
79
|
|
|
80
|
+
## Writing a schedule
|
|
81
|
+
|
|
82
|
+
`Set`-ing `Schedule1`/`Schedule2` with every point's own encoded bytes
|
|
83
|
+
concatenated as one multi-element payload (same "every point is a
|
|
84
|
+
separate element" shape the `Get` response already uses) replaces the
|
|
85
|
+
whole schedule in a single request. This is `set_light_schedule()` in
|
|
86
|
+
`device.py`.
|
|
87
|
+
|
|
88
|
+
Two requirements, both load-bearing — get either wrong and the device
|
|
89
|
+
accepts the write (a normal FSCI success status) but silently leaves
|
|
90
|
+
the schedule unchanged, with no error surfaced anywhere:
|
|
91
|
+
|
|
92
|
+
- **Every point must define this light's full channel set**
|
|
93
|
+
(`get_supported_channels()`), not a subset. A partial channel set is
|
|
94
|
+
accepted but has no effect; the full set takes effect correctly.
|
|
95
|
+
- **The write must be padded to the device's own full reported
|
|
96
|
+
capacity** for `Schedule1`/`Schedule2` (`get_supported_attributes()`'s
|
|
97
|
+
own index count for that attribute), not just `len(points)` —
|
|
98
|
+
otherwise indexes beyond `len(points)` keep whatever was stored there
|
|
99
|
+
before. Padding entries are all-zero bytes (`time=0, flags=0, no
|
|
100
|
+
channels`); `flags` lacking `ACTIVE` (see the flags table above) is
|
|
101
|
+
what makes a padding entry read back as an empty slot.
|
|
102
|
+
|
|
103
|
+
`set_light_schedule()` raises `ValueError` before attempting any write
|
|
104
|
+
if a point is missing a required channel, or if `len(points)` itself
|
|
105
|
+
exceeds the reported capacity — both checkable ahead of time, so
|
|
106
|
+
neither needs a round-trip to the device to catch.
|
|
107
|
+
|
|
108
|
+
## Schedule groups (`GroupBitmap`, attribute 108)
|
|
109
|
+
|
|
110
|
+
Multiple lights can share one schedule: editing one and saving pushes
|
|
111
|
+
the same schedule to every OTHER light whose `GroupBitmap` matches,
|
|
112
|
+
each as its own independent GATT write (see `Tank.java`'s own
|
|
113
|
+
`createSave()` call site). Not to be confused with `GroupMaster`/
|
|
114
|
+
`IsGroupMaster` (900/911, `GroupInfo`/`get_group_info()`) — an
|
|
115
|
+
unrelated, dead attribute pair with nothing to do with schedule
|
|
116
|
+
sharing.
|
|
117
|
+
|
|
118
|
+
A 64-bit value (`Tank.createGroupBitmap()`/`parseGroupBitmap()`/
|
|
119
|
+
`parseSubGroupBitmap()` in `Tank.java`): bit 0 always set, bits 1–30
|
|
120
|
+
a main group number (2–31), bits 31–62 an optional per-fixture
|
|
121
|
+
sub-group. **Matching is exact equality of the whole 64-bit value**,
|
|
122
|
+
not overlapping bits.
|
|
123
|
+
|
|
124
|
+
Observed values, two Radion lights on the same tank:
|
|
125
|
+
|
|
126
|
+
| State | Light A | Light B |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| Same schedule group | `0x4000000000000003` (bits 0, 1, 62) | `0x4000000000000003` (bits 0, 1, 62) |
|
|
129
|
+
| Split into separate groups | `0x2000000000000003` (bits 0, 1, 61) | `0x4000000000000003` (bits 0, 1, 62) |
|
|
130
|
+
|
|
131
|
+
Bit 1 (main group) is the same in both rows; only the subgroup bit (61
|
|
132
|
+
vs 62) differs once split — matching the bit layout above.
|
|
133
|
+
|
|
134
|
+
Every light belongs to a group in the app's own UI, including a group
|
|
135
|
+
of one — there is no ungrouped state exposed there. A light whose
|
|
136
|
+
`group_mask` (`get_device_info()`'s own field) is `None` is treated as
|
|
137
|
+
its own exclusive schedule group by this library, matched with no
|
|
138
|
+
other light regardless of that other light's own value.
|
|
139
|
+
|
|
140
|
+
`mobius.cli`'s own `--load-schedule-file`/`--load-mob-file` use this
|
|
141
|
+
exact-equality rule to replicate the app's own group-propagation
|
|
142
|
+
behavior: for a light with a real `group_mask`, both flags also write
|
|
143
|
+
to every other light discovered on the same Thread mesh
|
|
144
|
+
(`discovery.discover_tank()`) whose own `group_mask` matches exactly,
|
|
145
|
+
each reached via `RelayedMobiusDevice` — see `cli._resolve_group_targets()`.
|
|
146
|
+
A light with no `group_mask`, or a pump (which has no such grouping
|
|
147
|
+
concept at all), only ever writes to the one device named.
|
|
148
|
+
|
|
75
149
|
## Interpolation algorithm
|
|
76
150
|
|
|
77
151
|
Confirmed via reverse engineering the app's own schedule-interpolation logic:
|
|
@@ -82,6 +156,10 @@ Confirmed via reverse engineering the app's own schedule-interpolation logic:
|
|
|
82
156
|
"the next day," i.e. its time + 1440).
|
|
83
157
|
3. **`Brightness` (`VisualID.Brightness`) is a master dimmer, applied to
|
|
84
158
|
EACH bracketing point separately, before interpolating between them**
|
|
159
|
+
(the app's own UI calls this channel "Point Intensity," not
|
|
160
|
+
"Brightness" -- not to be confused with the separate,
|
|
161
|
+
whole-schedule `Schedule1Intensity` master dimmer above, which
|
|
162
|
+
applies once to the whole schedule rather than per-point)
|
|
85
163
|
-- confirmed via reverse engineering the app's own "flatten" logic, called on
|
|
86
164
|
both point copies before the
|
|
87
165
|
interpolation loop -- an earlier decompilation attempt for this logic
|
|
@@ -360,3 +438,17 @@ their own attributes.
|
|
|
360
438
|
confirmed; this separate, rarer modifier is not, and is treated as
|
|
361
439
|
always-disabled by those modifiers.
|
|
362
440
|
- Writing/programming a schedule (only reading is implemented).
|
|
441
|
+
|
|
442
|
+
## One combined round-trip: `get_light_poll_batch()`
|
|
443
|
+
|
|
444
|
+
Everything on this page — the schedule fetch, and every
|
|
445
|
+
lunar/acclimation/insolation-related attribute
|
|
446
|
+
`process_light_intensities()` needs — is also available as ONE combined
|
|
447
|
+
round-trip via `get_light_poll_batch()`, rather than the schedule fetch
|
|
448
|
+
plus several further, conditional individual reads. See
|
|
449
|
+
[14-batched-attribute-reads.md](./14-batched-attribute-reads.md) for the
|
|
450
|
+
full confirmation and how it fits alongside `get_metadata_batch()`/
|
|
451
|
+
`get_full_poll_batch()`. The decode logic itself is identical either
|
|
452
|
+
way — both paths call the same pure functions this page's own formulas
|
|
453
|
+
describe.
|
|
454
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Pump schedule
|
|
2
|
+
|
|
3
|
+
## Same attribute, same `Point` framing, different primitive and different semantics
|
|
4
|
+
|
|
5
|
+
Pumps use the exact same `Schedule1`/`Schedule2` attributes and the exact
|
|
6
|
+
same `time + flags + primitiveData` point framing as lights (confirmed: the
|
|
7
|
+
app's own support check only checks whether `Schedule1` is a supported
|
|
8
|
+
attribute at all — it isn't gated by `PrimitiveType`). What differs:
|
|
9
|
+
|
|
10
|
+
- `primitiveData` decodes as a **`PumpPrimitive`**, not a `LightPrimitive`.
|
|
11
|
+
- Semantics are **discrete blocks, not a continuous curve**. `Point.getEnd()`
|
|
12
|
+
in the decompile literally returns "the next point's start time" — a
|
|
13
|
+
point's mode and parameters are the exact active setting from its own
|
|
14
|
+
start time until the next point begins. There is no blending between two
|
|
15
|
+
pump speeds the way there is between two light-channel intensities.
|
|
16
|
+
|
|
17
|
+
This library's `get_active_pump_block()` reflects that: it's a lookup
|
|
18
|
+
(which point's `[start, next_start)` range contains "now"), not an
|
|
19
|
+
interpolation.
|
|
20
|
+
|
|
21
|
+
## `PumpPrimitive` wire format
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
mode (u8)
|
|
25
|
+
params (variable, depends on mode)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `PumpMode` and its own parameters
|
|
29
|
+
|
|
30
|
+
Confirmed literal values (note the gap: value `11` is genuinely unused
|
|
31
|
+
in the source) and each mode's own full parameter set, type, and size
|
|
32
|
+
(byte offset within `params` is cumulative, in the listed order,
|
|
33
|
+
starting right after the mode byte) — everything needed to build a
|
|
34
|
+
valid `PumpPrimitiveValue` for that mode in one place, rather than
|
|
35
|
+
split across separate mode/parameter/size tables:
|
|
36
|
+
|
|
37
|
+
| Value | Mode | Parameters (type, size) |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 0 | `Undefined` | *(none)* — the padding placeholder `set_pump_schedule()` itself writes for an empty slot |
|
|
40
|
+
| 1 | `ConstantSpeed` | `MaxSpeed` (int16, 2B) |
|
|
41
|
+
| 2 | `Lagoon` | `MaxSpeed` (int16, 2B) |
|
|
42
|
+
| 3 | `ReefCrest` | `MaxSpeed` (int16, 2B) |
|
|
43
|
+
| 4 | `NutrientTransport` | `MaxSpeed` (int16, 2B) |
|
|
44
|
+
| 5 | `TidalSwell` | `MaxSpeed` (int16, 2B) |
|
|
45
|
+
| 6 | `ShortPulse` | `MaxSpeed` (int16, 2B), `Time` (int32, 4B, **+250 offset** — see below) |
|
|
46
|
+
| 7 | `Gyre` | `MaxSpeed` (int16, 2B), `BigTime` (int32, 4B) |
|
|
47
|
+
| 8 | `Transition` | `RampType` (byte enum, 1B — see `RampType` below) |
|
|
48
|
+
| 9 | `ExpandingPulse` | `MaxSpeed` (int16, 2B), `StartTime` (int16, 2B, **+250 offset**), `EndTime` (int16, 2B, **+250 offset**) |
|
|
49
|
+
| 10 | `Sync` | `MaxSpeed` (int16, 2B), `PhaseShift` (int16, 2B), `Master` (8 raw bytes — see below) |
|
|
50
|
+
| 12 | `EcoSmartBack` | `MaxSpeed` (int16, 2B), `PhaseShift` (int16, 2B), `Master` (8 raw bytes) |
|
|
51
|
+
| 13 | `Feed` | `MaxSpeed` (int16, 2B) |
|
|
52
|
+
| 14 | `BatteryBackup` | `MaxSpeed` (int16, 2B) |
|
|
53
|
+
| 15 | `Random` | `MinSpeed` (int16, 2B), `MaxSpeed` (int16, 2B), `Variance` (int16, 2B) |
|
|
54
|
+
| 16 | `Pulse` | `MaxSpeed` (int16, 2B), `OnTime` (int32, 4B), `OffTime` (int32, 4B) |
|
|
55
|
+
|
|
56
|
+
All multi-byte integers are little-endian. `Master`'s own 8 bytes are
|
|
57
|
+
the last 8 bytes of the BLE address of the pump this one syncs to, not
|
|
58
|
+
a numeric field. `RampType` is a separate byte-valued enum, not a
|
|
59
|
+
plain integer — see its own table below.
|
|
60
|
+
|
|
61
|
+
## Which modes a given pump actually supports
|
|
62
|
+
|
|
63
|
+
The app does not offer all 16 `PumpMode` values on every pump.
|
|
64
|
+
`M.PumpMode.supportedModes(PrimitiveType, Object)` (decompiled app,
|
|
65
|
+
`com/c2/comm/M.java`) returns a different mode list per
|
|
66
|
+
`PrimitiveType` (the pump's own product family, not a separate
|
|
67
|
+
per-model table) — and this is a genuinely enforced rule, not just a
|
|
68
|
+
UI display filter: `BlockSchedule.removeUnsupportedModes()` calls this
|
|
69
|
+
same function and actively downgrades any point whose mode isn't in
|
|
70
|
+
the returned list to `ConstantSpeed` before further use.
|
|
71
|
+
|
|
72
|
+
This library exposes the same logic as `supported_pump_modes()`:
|
|
73
|
+
|
|
74
|
+
| `PrimitiveType` | Supported modes |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `PumpV1`, `AlpacaV1` | `ConstantSpeed`, `Random`, `Pulse`, `Sync`, `Feed` |
|
|
77
|
+
| `VorTechV1` | `ConstantSpeed`, `ReefCrest`, `Lagoon`, `ShortPulse`, `Feed`, `TidalSwell`, `NutrientTransport`, `Sync`, `EcoSmartBack`, `ExpandingPulse`, `Gyre`, `Transition` |
|
|
78
|
+
| `VectraV1`, closed-loop | `ConstantSpeed`, `ReefCrest`, `Lagoon`, `Gyre`, `Feed` |
|
|
79
|
+
| `VectraV1`, open-loop (or unknown) | `ConstantSpeed`, `Feed` |
|
|
80
|
+
| `TurtleV1`, `CoffeeV1` | `ConstantSpeed`, `Feed` |
|
|
81
|
+
| `Undefined`, `VisualV1`, `DoseV1`, `HotSauceV1` | *(none — this mechanism was never wired up for these)* |
|
|
82
|
+
|
|
83
|
+
`VectraV1` is the one primitive type where the result additionally
|
|
84
|
+
depends on a live, per-device value: whether that specific Vectra is
|
|
85
|
+
in "closed loop" mode (`VectraInfo.closed_loop`, from
|
|
86
|
+
`get_vectra_info()` — `ClosedLoop`/attribute `801`, a real settings-
|
|
87
|
+
screen toggle per that dataclass's own docstring, not something
|
|
88
|
+
inferrable from `PrimitiveType` alone). The decompiled app's own two
|
|
89
|
+
real call sites (`BlockInfoSliderView`, `BlockSchedule`) both use the
|
|
90
|
+
single-argument `supportedModes(PrimitiveType)` overload, which passes
|
|
91
|
+
a literal `null` through — so `supported_pump_modes()` defaults
|
|
92
|
+
`closed_loop` to `None` and treats that identically to `False` (the
|
|
93
|
+
smaller, open-loop list), matching the app's own real-world behavior
|
|
94
|
+
rather than guessing permissively when this value hasn't been fetched.
|
|
95
|
+
|
|
96
|
+
**`BatteryBackup`(14) is real** — a genuine `PumpMode` value with the
|
|
97
|
+
real, documented parameter set above — but is never returned by this
|
|
98
|
+
function for any `PrimitiveType`, confirmed directly from the
|
|
99
|
+
decompiled app's own logic. Whatever real device or app flow assigns
|
|
100
|
+
that mode, it isn't through this schedule-editor mode-picker path.
|
|
101
|
+
|
|
102
|
+
### The `+250` offset quirk
|
|
103
|
+
|
|
104
|
+
`Time`, `StartTime`, and `EndTime` (**only these three** — not `OnTime`/
|
|
105
|
+
`OffTime`/`BigTime`, which are stored raw) are stored as
|
|
106
|
+
`actual_value - 250` on the wire, and the app adds `250` back when reading.
|
|
107
|
+
`250` is a real Android library constant
|
|
108
|
+
— another instance of the symbolic-substitution artifact described in
|
|
109
|
+
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md), resolved by
|
|
110
|
+
looking up that library constant's real value. Verified against
|
|
111
|
+
synthetic round-trip data (`tests/test_pump_schedule.py`).
|
|
112
|
+
|
|
113
|
+
### `MaxSpeed`/`MinSpeed` sign
|
|
114
|
+
|
|
115
|
+
Negative values encode reverse rotation direction (per `getReverse()` in
|
|
116
|
+
the decompile, which separately inspects the sign). This library reports
|
|
117
|
+
the raw signed value as-is rather than replicating the app's
|
|
118
|
+
sign-stripping/`getReverse()` split — check the sign yourself if direction
|
|
119
|
+
matters for your use case.
|
|
120
|
+
|
|
121
|
+
### `RampType`
|
|
122
|
+
|
|
123
|
+
Confirmed literal values:
|
|
124
|
+
|
|
125
|
+
| Value | Name |
|
|
126
|
+
|---|---|
|
|
127
|
+
| 1 | `Sinusoidal` |
|
|
128
|
+
| 2 | `Logarithmic` |
|
|
129
|
+
| 3 | `Linear` |
|
|
130
|
+
|
|
131
|
+
## Live verification
|
|
132
|
+
|
|
133
|
+
Fetched an 11-point schedule from a real VorTech MP40QD pump; the point
|
|
134
|
+
active at the time of the test decoded as `TidalSwell` mode with a single
|
|
135
|
+
`MaxSpeed` parameter (`300`) — exactly matching the parameter table above
|
|
136
|
+
for that mode (`TidalSwell` → `[MaxSpeed]` only), with a plausible value.
|
|
137
|
+
|
|
138
|
+
## Avoiding a redundant re-fetch
|
|
139
|
+
|
|
140
|
+
`get_current_pump_block()` accepts an optional `points` parameter — pass
|
|
141
|
+
an already-fetched schedule (e.g. from a caller that already called
|
|
142
|
+
`get_pump_schedule()` itself for its own purposes, such as counting
|
|
143
|
+
points) to skip this method's own otherwise-redundant re-fetch of the
|
|
144
|
+
exact same attribute. `None` (the default) still fetches it fresh, same
|
|
145
|
+
as always.
|
|
146
|
+
|
|
147
|
+
## Writing a pump schedule
|
|
148
|
+
|
|
149
|
+
Same mechanism as lights (see
|
|
150
|
+
[06-light-schedule.md](./06-light-schedule.md)'s own "Writing a
|
|
151
|
+
schedule" section): `Set`-ing `Schedule1`/`Schedule2` with every
|
|
152
|
+
point's own encoded bytes concatenated as one multi-element payload,
|
|
153
|
+
always padded to the device's own full reported capacity for that
|
|
154
|
+
attribute so a shorter write doesn't leave stale points at higher
|
|
155
|
+
indexes. This is `set_pump_schedule()` in `device.py`.
|
|
156
|
+
|
|
157
|
+
**Every point must also be padded to a FIXED size** —
|
|
158
|
+
`MAX_PUMP_SCHEDULE_POINT_SIZE` (16 bytes: 2 time + 1 flags + 1 mode +
|
|
159
|
+
the largest per-mode parameter total, currently `Sync`/`EcoSmartBack`'s
|
|
160
|
+
own `MaxSpeed`+`PhaseShift`+`Master`) — not just the longest point
|
|
161
|
+
within the current write. This differs from lights, which only need
|
|
162
|
+
padding to the longest point in that same write (see the Set-attribute
|
|
163
|
+
request format in
|
|
164
|
+
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md): one
|
|
165
|
+
shared `length` field for the whole request, every element in a write
|
|
166
|
+
must already be the same length). A device is consistent with
|
|
167
|
+
expecting every schedule slot to be sized for the largest mode it can
|
|
168
|
+
ever hold, regardless of which mode a given point actually uses — a
|
|
169
|
+
short, unpadded write (e.g. a single `ConstantSpeed` point, naturally
|
|
170
|
+
6 bytes) is accepted (normal FSCI success) but has no effect; the same
|
|
171
|
+
point padded to 16 bytes takes effect correctly.
|
|
172
|
+
|
|
173
|
+
Padding entries (for the capacity padding above) are
|
|
174
|
+
`PumpSchedulePoint(0, 0, PumpPrimitiveValue(PumpMode.Undefined, {}))`,
|
|
175
|
+
also padded out to the same fixed 16-byte size.
|
|
176
|
+
|
|
177
|
+
Unlike lights, there's no "full channel set" requirement here — each
|
|
178
|
+
mode's own required parameters are already strictly checked by
|
|
179
|
+
`PumpPrimitiveValue.encode()` itself (raises if one is missing).
|
|
180
|
+
|
|
181
|
+
## Not implemented here
|
|
182
|
+
|
|
183
|
+
- The `Master` field's BLE-address matching against known devices in a
|
|
184
|
+
group (this library returns the raw 8 bytes; matching it against a scan
|
|
185
|
+
result's address suffix is left to the caller).
|
|
@@ -161,6 +161,21 @@ unreachable when propagation happens), or whether writing via
|
|
|
161
161
|
same way -- see `set_time_to_now()`'s own docstring for the full
|
|
162
162
|
breakdown.
|
|
163
163
|
|
|
164
|
+
## Scene activation (`start_scene()`/`start_feed_mode()`) -- same mechanism, one level less confirmed
|
|
165
|
+
|
|
166
|
+
`start_scene()` defaults to `broadcast=True`, using the exact same
|
|
167
|
+
`group=1` reserved-byte mechanism confirmed above for `set_time_to_now()`
|
|
168
|
+
-- the app's own scene-activation code path sets this same group value
|
|
169
|
+
on every `CurrentScene` write it sends. Unlike the time-sync write,
|
|
170
|
+
this specific application (`CurrentScene` propagating the same way
|
|
171
|
+
`Epoch` does) hasn't been independently confirmed against real
|
|
172
|
+
hardware the same way -- it's confirmed as the app's own real code
|
|
173
|
+
path, not yet its own separate real-hardware propagation test. See
|
|
174
|
+
[05-scenes.md](./05-scenes.md) for the full picture (the wire format,
|
|
175
|
+
the fallback via `broadcast=False`) and the same "group=1 only reaches
|
|
176
|
+
this one mesh, never a different tank's own" reasoning that applies
|
|
177
|
+
here too.
|
|
178
|
+
|
|
164
179
|
## Device control (`reboot()`) -- targeted, no mesh-wide propagation
|
|
165
180
|
|
|
166
181
|
`reboot()` writes `ResetType.Soft` (0) to `Reset` (6) -- confirmed via
|