python-mobius 0.9.0__tar.gz → 0.10.1__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.9.0 → python_mobius-0.10.1}/CHANGELOG.md +29 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/PKG-INFO +1 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/00-overview.md +2 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/01-ble-transport.md +5 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/03-attributes-and-opcodes.md +1 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/05-scenes.md +20 -2
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/09-thread-coap-relay.md +45 -3
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/11-device-status-attributes.md +13 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/14-attribute-reference.md +5 -5
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/16-python-library.md +23 -2
- python_mobius-0.10.1/documentation/17-configuration-checksums.md +88 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/pyproject.toml +1 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/__init__.py +13 -7
- python_mobius-0.10.1/src/mobius/checksums.py +119 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/cli.py +148 -19
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/constants.py +19 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/device.py +311 -41
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/device_status.py +5 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/discovery.py +19 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/manufacturer.py +6 -1
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/relay.py +50 -38
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/scenes.py +5 -1
- python_mobius-0.10.1/src/mobius/timezone.py +63 -0
- python_mobius-0.10.1/tests/test_bluetooth_only.py +119 -0
- python_mobius-0.10.1/tests/test_checksums.py +98 -0
- python_mobius-0.10.1/tests/test_configuration_cache.py +289 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_full_poll_batch.py +51 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_light_poll_batch.py +20 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_metadata_batch.py +18 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_relay.py +38 -19
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_scenes.py +71 -0
- python_mobius-0.10.1/tests/test_time_zone.py +68 -0
- python_mobius-0.10.1/tests/test_write_size.py +64 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/.gitignore +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/LICENSE +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/README.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/06-light-schedule.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/13-attribute-dump.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/documentation/15-mob-template-format.md +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/coap.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/crc.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/dump.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/frame.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/mesh_address.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/mob.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/modifiers.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/power.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/src/mobius/schedule.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_attributes_batch.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_battery_settings.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_cli_group_targets.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_cli_mob_file.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_cli_schedule_file.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_coap.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_device_control.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_device_status.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_device_summary.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_device_time.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_discovery_company_ids.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_dump.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_extra_physical_values.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_fan_thresholds.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_frame.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_group_bitmap.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_light_intensity_branching.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_mob.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_modifiers.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_networked_thread_devices.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_new_diagnostic_attributes.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_power.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_param_ranges.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_reverse.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_status.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_supported_modes.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_pump_telemetry.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_schedule_file_format.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_serial_identity.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_shared_helpers.py +0 -0
- {python_mobius-0.9.0 → python_mobius-0.10.1}/tests/test_supported_attributes.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1
|
|
4
|
+
|
|
5
|
+
- Fixed lights not being read through a gateway: the poll's reply was too
|
|
6
|
+
large to be relayed. The schedule and scenes are now read in requests
|
|
7
|
+
of their own, as the app does.
|
|
8
|
+
|
|
9
|
+
## 0.10.0
|
|
10
|
+
|
|
11
|
+
- Added `set_time_zone()`; the batched polls now include the device's time
|
|
12
|
+
zone strings.
|
|
13
|
+
- Relayed requests now accept a late response to the first attempt, as
|
|
14
|
+
the app does.
|
|
15
|
+
- Fixed an unreachable light returning an empty poll as a success.
|
|
16
|
+
- Added configuration checksums (`get_configuration_checksums()`, CLI
|
|
17
|
+
`--check-checksums`).
|
|
18
|
+
- `get_full_poll_batch(cache=...)` reads scenes and schedules only when
|
|
19
|
+
their checksum changed.
|
|
20
|
+
- `start_scene()` now runs a scene for its configured length by default
|
|
21
|
+
(a duration of 0 makes the device reuse its last scene length).
|
|
22
|
+
- Fixed a scene briefly reporting about 18 hours remaining as it ends.
|
|
23
|
+
- Added Bluetooth-only device support (Blades, MXM): `is_bluetooth_only()`
|
|
24
|
+
and `find_bluetooth_only_tank_members()`. The CLI connects to them
|
|
25
|
+
directly and includes them in schedule group writes.
|
|
26
|
+
- Writes use the negotiated MTU instead of 20-byte fragments, as the app
|
|
27
|
+
does (fewer writes per request).
|
|
28
|
+
- The pump's operation state is now part of the batched poll.
|
|
29
|
+
- `ConfigurationCache(static_refresh_interval=...)` reads rarely-changing
|
|
30
|
+
attributes (identity, firmware, settings) only at that interval.
|
|
31
|
+
|
|
3
32
|
## 0.9.0
|
|
4
33
|
|
|
5
34
|
- **Breaking:** `RelayedMobiusDevice` writes now raise an error when the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
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
|
|
@@ -27,6 +27,7 @@ and the `danmrossi/MobiusControl` project) and testing on real devices.
|
|
|
27
27
|
| Reach many devices through one BLE connection | [09](./09-thread-coap-relay.md) |
|
|
28
28
|
| Read settings, diagnostics or send commands (reboot, set time) | [11](./11-device-status-attributes.md) |
|
|
29
29
|
| Dump every attribute of a device | [13](./13-attribute-dump.md) |
|
|
30
|
+
| Avoid re-reading scenes, schedules or the attribute table | [17](./17-configuration-checksums.md) |
|
|
30
31
|
| Know what is not covered or not tested | [10](./10-known-gaps-and-open-questions.md) |
|
|
31
32
|
|
|
32
33
|
## Document map
|
|
@@ -49,6 +50,7 @@ and the `danmrossi/MobiusControl` project) and testing on real devices.
|
|
|
49
50
|
| [14-attribute-reference.md](./14-attribute-reference.md) | Every known attribute ID and enum value |
|
|
50
51
|
| [15-mob-template-format.md](./15-mob-template-format.md) | The app's `.mob` schedule template file |
|
|
51
52
|
| [16-python-library.md](./16-python-library.md) | How this Python library maps onto the protocol; CLI |
|
|
53
|
+
| [17-configuration-checksums.md](./17-configuration-checksums.md) | Checksums of the attribute table, scenes and schedules |
|
|
52
54
|
|
|
53
55
|
## Conventions
|
|
54
56
|
|
|
@@ -31,7 +31,11 @@ an `RX_FINAL` notification appends its bytes and completes the frame. Parse
|
|
|
31
31
|
the buffer and clear it.
|
|
32
32
|
|
|
33
33
|
20 bytes (the BLE 4.0 default ATT payload) always works; a larger
|
|
34
|
-
negotiated MTU only reduces the number of writes.
|
|
34
|
+
negotiated MTU only reduces the number of writes. The app asks for an MTU
|
|
35
|
+
of 500 and writes chunks of the negotiated MTU minus 5; `MobiusDevice`
|
|
36
|
+
does the same with the MTU the Bluetooth stack negotiated (20 bytes when
|
|
37
|
+
it's unknown or 23), unless given a fixed `mtu`. Over a Bluetooth proxy
|
|
38
|
+
every write is a network round trip, so this matters most there.
|
|
35
39
|
|
|
36
40
|
## Several requests in flight
|
|
37
41
|
|
|
@@ -126,6 +126,7 @@ that device. The app uses:
|
|
|
126
126
|
|
|
127
127
|
| Write | Attribute | Group |
|
|
128
128
|
|---|---|---|
|
|
129
|
+
| Set the time zone | `PosixTimeZoneString` (205), `OlsonTimeZoneLocation` (206) | 1 |
|
|
129
130
|
| Set the clock | `Epoch` (201) = current Unix time, u32 | 1. Rejected with group 0 |
|
|
130
131
|
| Start a scene | `CurrentScene` (401) | The tank's group ([05](./05-scenes.md)) |
|
|
131
132
|
| Cancel a scene | `OperationState` (104) = 3 | The tank's group ([05](./05-scenes.md#cancelling-a-scene)) |
|
|
@@ -58,7 +58,9 @@ SceneTimer (403): remaining seconds (u16)
|
|
|
58
58
|
SceneTimeout (402): total seconds of the running scene (u16)
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
A scene is running when the id is not 0 and `SceneTimer` is above 0
|
|
61
|
+
A scene is running when the id is not 0 and `SceneTimer` is above 0 (and
|
|
62
|
+
not above `SceneTimeout`; see below). Right after a scene is started,
|
|
63
|
+
`SceneTimer` can still read 0 for a moment. The
|
|
62
64
|
second half of `CurrentScene` doesn't change while the scene runs; the
|
|
63
65
|
countdown is `SceneTimer`. The app reads these three attributes once and
|
|
64
66
|
counts the remaining time down itself (end time = now + `SceneTimer`).
|
|
@@ -70,16 +72,32 @@ Example (idle VorTech pump): `CurrentScene` `00 00 00 00`, `SceneTimer`
|
|
|
70
72
|
Write `CurrentScene`:
|
|
71
73
|
|
|
72
74
|
```
|
|
73
|
-
id (u16) + duration (u16, seconds
|
|
75
|
+
id (u16) + duration (u16, seconds)
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
A duration of 0 makes the device reuse the length of its last scene run
|
|
79
|
+
(`SceneTimeout`), not the scene's configured timeout: on a VorTech whose
|
|
80
|
+
Feed Mode is configured for 300 s, starting Feed Mode with 20 and later
|
|
81
|
+
with 0 ran it for 20 s both times. `start_scene()` therefore writes the
|
|
82
|
+
scene's configured timeout from `ConfiguredScenes` unless a duration is
|
|
83
|
+
given.
|
|
84
|
+
|
|
76
85
|
The app writes 0 as the duration and sends the write to one device with
|
|
77
86
|
the reserved field set to the **tank's group**
|
|
78
87
|
([02](./02-framing-and-crc.md#reserved-field)), so every device of the tank
|
|
79
88
|
starts the scene. With group 0 only the addressed device changes.
|
|
89
|
+
Bluetooth-only devices (Blades) are not on the mesh, so the group write
|
|
90
|
+
doesn't reach them; the app sends them the same write directly
|
|
91
|
+
([09](./09-thread-coap-relay.md#bluetooth-only-devices)).
|
|
80
92
|
|
|
81
93
|
To add time to the running scene, the app writes the same id with
|
|
82
94
|
`remaining + extra` seconds (at most 65535), also to the tank's group.
|
|
95
|
+
Written with group 0, the same write doesn't change the running scene
|
|
96
|
+
(the countdown continues); with the tank's group it's untested.
|
|
97
|
+
|
|
98
|
+
As a scene ends, `SceneTimer` can briefly read just below 65536 (the
|
|
99
|
+
timer passing zero); a `SceneTimer` above `SceneTimeout` means the scene
|
|
100
|
+
has ended.
|
|
83
101
|
|
|
84
102
|
## Cancelling a scene
|
|
85
103
|
|
|
@@ -143,9 +143,12 @@ Request types: 0 Con, 1 Non, 2 Ack, 3 Rst. Methods: 1 GET, 2 POST, 3 PUT,
|
|
|
143
143
|
|
|
144
144
|
### Timeouts and retries
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
Wait 5 s for the response. On a timeout, send the same request again
|
|
147
|
+
with the **same token** and wait another 5 s; a response to either send
|
|
148
|
+
completes the request. This is what the app does, and it matters: a
|
|
149
|
+
relayed response can arrive after the first 5 s, and with a new token for
|
|
150
|
+
the resend that late response would be discarded. When both waits time
|
|
151
|
+
out, the route to that target is probably down for a while; failing
|
|
149
152
|
further requests to it immediately until the next poll cycle avoids
|
|
150
153
|
waiting on every one.
|
|
151
154
|
|
|
@@ -153,8 +156,47 @@ A gateway can lose its route to a single target while everything else
|
|
|
153
156
|
keeps working. The protocol has no command to rebuild the mesh; switching
|
|
154
157
|
to another device as gateway is the practical recovery.
|
|
155
158
|
|
|
159
|
+
## Reply size
|
|
160
|
+
|
|
161
|
+
A relayed reply that is too large never arrives: the request times out
|
|
162
|
+
as if the target were unreachable, while smaller requests to the same
|
|
163
|
+
target keep working. Seen with a Radion XR15 G6 relayed through a VorTech
|
|
164
|
+
MP40: a poll batch whose reply was about 2 KB (schedule, scenes and
|
|
165
|
+
metadata together) always timed out, the same data read in three requests
|
|
166
|
+
arrived. The limit is somewhere above 1.1 KB (a 25-point light schedule
|
|
167
|
+
alone, about 1.05 KB of data, arrives).
|
|
168
|
+
|
|
169
|
+
The app reads the schedule (and the Insolation schedule and driver
|
|
170
|
+
information) in a request of its own, and everything else together; the
|
|
171
|
+
Insolation table even takes 8 requests. `get_full_poll_batch()` does the
|
|
172
|
+
same with the schedule and `ConfiguredScenes`.
|
|
173
|
+
|
|
156
174
|
## Writes and the mesh
|
|
157
175
|
|
|
158
176
|
A relayed Set reaches only its target. A Set sent with the reserved
|
|
159
177
|
field's group = 1 is forwarded by the receiving device to the whole mesh
|
|
160
178
|
instead; see [03](./03-attributes-and-opcodes.md#group-writes).
|
|
179
|
+
|
|
180
|
+
## Bluetooth-only devices
|
|
181
|
+
|
|
182
|
+
Some devices share a tank's pan_id in their advertisement but are not on
|
|
183
|
+
its Thread mesh: they have no mesh address, don't appear in any device's
|
|
184
|
+
`NetworkedThreadDevices`, and can't be relayed to. They are the Blade
|
|
185
|
+
models and MXM (`BLUETOOTH_ONLY_MODELS`, `is_bluetooth_only()`; the app
|
|
186
|
+
keeps the same list). A Blade reports `RadioType` ESP32C3.
|
|
187
|
+
|
|
188
|
+
The app handles them as follows:
|
|
189
|
+
|
|
190
|
+
- A request to a Bluetooth-only device always goes over that device's own
|
|
191
|
+
connection, never through the gateway.
|
|
192
|
+
- A Bluetooth-only device is never chosen as the device to connect to for
|
|
193
|
+
the rest of the tank, unless the tank has no other device.
|
|
194
|
+
- A tank-wide write (scenes, cancelling a scene, time sync, restart all)
|
|
195
|
+
is sent once through a mesh device with the tank's group, which reaches
|
|
196
|
+
the mesh, and once more directly to each Bluetooth-only device. A group
|
|
197
|
+
write sent over a Bluetooth-only device's connection reaches only that
|
|
198
|
+
device.
|
|
199
|
+
|
|
200
|
+
`find_bluetooth_only_tank_members(pan_id)` lists the Bluetooth-only
|
|
201
|
+
devices advertising a tank's pan_id. `RelayedMobiusDevice` refuses a peer
|
|
202
|
+
whose model is Bluetooth-only.
|
|
@@ -131,7 +131,18 @@ encoding unknown. Read them as raw bytes.
|
|
|
131
131
|
| `RTCTime` | 219 | u32 Unix seconds from the real-time clock |
|
|
132
132
|
| `IsClockSet` | 200 | bool |
|
|
133
133
|
|
|
134
|
-
The device's local date is the UTC date of `LocalTime`.
|
|
134
|
+
The device's local date is the UTC date of `LocalTime`. The device
|
|
135
|
+
derives `LocalTime` from `Epoch` and its POSIX TZ string (205), so a wrong
|
|
136
|
+
time zone shifts schedules and the lunar date.
|
|
137
|
+
|
|
138
|
+
**How the app keeps the clock right:** it doesn't sync on a timer. When it
|
|
139
|
+
loads a tank it compares the tank's `Epoch` with the phone's clock, and if
|
|
140
|
+
they differ by more than 15 minutes it shows "Your device's clock is
|
|
141
|
+
incorrect", which the user taps to fix (unless the time was deliberately
|
|
142
|
+
set by hand). The fix writes the time zone (205 and 206, from the phone's
|
|
143
|
+
time zone), then `Epoch`, all with group 1. The POSIX string of an Olson
|
|
144
|
+
zone name is the last line of its TZif file (for example `Europe/Lisbon`
|
|
145
|
+
→ `WET0WEST,M3.5.0/1,M10.5.0`). For lunar
|
|
135
146
|
calculations see [06](./06-light-schedule.md#the-lunar-date).
|
|
136
147
|
|
|
137
148
|
The app's "Time Difference" troubleshooting check compares `Epoch` and
|
|
@@ -142,6 +153,7 @@ the schedule computation, which uses the client's clock.
|
|
|
142
153
|
|
|
143
154
|
| Command | Write | Reserved group |
|
|
144
155
|
|---|---|---|
|
|
156
|
+
| Set the time zone | `PosixTimeZoneString` (205) and `OlsonTimeZoneLocation` (206), each a 40-byte zero-padded string | 1 |
|
|
145
157
|
| Set the time | `Epoch` (201) = current Unix time (u32) | **1** (required; forwarded to the whole tank) |
|
|
146
158
|
| Reboot one device | `Reset` (6) = 0 (Soft) | 0 |
|
|
147
159
|
| Battery backup speed | `BatteryBackupSpeed` (706) = i16 permille | 0 |
|
|
@@ -23,9 +23,9 @@ Integers are little-endian; see the conventions in
|
|
|
23
23
|
| 9 | `NumberOfFsciInstances` | u8 | |
|
|
24
24
|
| 10 | `MaxFSCIPacketSize` | u16 | |
|
|
25
25
|
| 11 | `BootCounter` | u32 | Boot counter |
|
|
26
|
-
| 12 | `CRCAttributes` | — | Reads as 32 × 0xFFFF on every device seen |
|
|
26
|
+
| 12 | `CRCAttributes` | — | Reads as 32 × 0xFFFF on every device seen. [17](./17-configuration-checksums.md#crcattributes-12-and-crcfunction-13) |
|
|
27
27
|
| 13 | `CRCFunction` | u16 | |
|
|
28
|
-
| 14 | `SupportedAttributesCRC` | u16 |
|
|
28
|
+
| 14 | `SupportedAttributesCRC` | u16 | Checksum of the supported attribute table. [17](./17-configuration-checksums.md#supportedattributescrc-14) |
|
|
29
29
|
| 15 | `NumberOfPrimitives` | u8 | |
|
|
30
30
|
| 101 | `PhysicalValues` | indexed by PhysicalValueID; element = i32 (4 bytes) or raw | Supported indexes differ per device. [11](./11-device-status-attributes.md#pump-telemetry) |
|
|
31
31
|
| 102 | `PowerState` | u8 | |
|
|
@@ -76,12 +76,12 @@ Integers are little-endian; see the conventions in
|
|
|
76
76
|
| 405 | `LiveDemoTimeout` | u16 | |
|
|
77
77
|
| 406 | `LiveDemoTimer` | u16 | |
|
|
78
78
|
| 407 | `LiveDemoScene` | raw | Length differs between lights and pumps |
|
|
79
|
-
| 408 | `ConfiguredScenesChecksum` | u16 |
|
|
79
|
+
| 408 | `ConfiguredScenesChecksum` | u16 | CRC16 of the used scene slots. [17](./17-configuration-checksums.md#configuredsceneschecksum-408) |
|
|
80
80
|
| 500 | `Schedule1` | list of schedule points | [06](./06-light-schedule.md), [07](./07-pump-schedule.md). Index count = capacity |
|
|
81
|
-
| 501 | `Schedule1Checksum` |
|
|
81
|
+
| 501 | `Schedule1Checksum` | u16 | CRC16 of the active points. [17](./17-configuration-checksums.md#schedule1checksum-501-schedule2checksum-504) |
|
|
82
82
|
| 502 | `Schedule1StartTime` | — | |
|
|
83
83
|
| 503 | `Schedule2` | list of schedule points | Insolation moon schedule (lights only); same format as 500 ([06](./06-light-schedule.md#moon-schedule-schedule2)) |
|
|
84
|
-
| 504 | `Schedule2Checksum` |
|
|
84
|
+
| 504 | `Schedule2Checksum` | u16 | CRC16 of the active points. [17](./17-configuration-checksums.md#schedule1checksum-501-schedule2checksum-504) |
|
|
85
85
|
| 505 | `Schedule2StartTime` | — | |
|
|
86
86
|
| 506 | `Schedule2ActiveDays` | — | |
|
|
87
87
|
| 507 | `CurrentSchedule` | — | |
|
|
@@ -18,6 +18,7 @@ documents. The README has installation and a quick start.
|
|
|
18
18
|
| `mobius.device_status`, `mobius.pump_status` | Result dataclasses ([11](./11-device-status-attributes.md)) |
|
|
19
19
|
| `mobius.mob` | `.mob` templates ([15](./15-mob-template-format.md)) |
|
|
20
20
|
| `mobius.dump` | Attribute dumps ([13](./13-attribute-dump.md)) |
|
|
21
|
+
| `mobius.checksums` | Configuration checksums ([17](./17-configuration-checksums.md)) |
|
|
21
22
|
| `mobius.cli` | The `mobius-scan` command |
|
|
22
23
|
|
|
23
24
|
## Connecting
|
|
@@ -57,8 +58,9 @@ Higher level, by area:
|
|
|
57
58
|
| Pumps | `get_pump_telemetry()`, `get_pump_flow_range()`, `get_pump_schedule()`, `set_pump_schedule()`, `get_current_pump_block()`, `get_pump_override_mode()`, `get_operation_mode()`, `get_battery_backup_info()`, `set_battery_backup_speed()`, `get_boosted_battery_info()`, `set_boosted_battery()`, `get_vectra_info()`, `get_coffee_info()` |
|
|
58
59
|
| Scenes | `get_configured_scenes()`, `get_current_scene()`, `start_scene()`, `start_feed_mode()`, `resume_schedule()`, `get_operation_state()`, `get_tank_group()` |
|
|
59
60
|
| Settings | `get_advanced_features()`, `set_advanced_features()`, `get_calibration_info()`, `get_maintenance_info()`, `get_group_info()`, `get_fan_thresholds()` |
|
|
60
|
-
| Commands | `set_time_to_now()`, `reboot()`, `reboot_all()` |
|
|
61
|
+
| Commands | `set_time_to_now()`, `set_time_zone()`, `reboot()`, `reboot_all()` |
|
|
61
62
|
| Clock | `get_device_time_info()`, `get_clock_sync_info()` |
|
|
63
|
+
| Checksums | `get_configuration_checksums()` |
|
|
62
64
|
| Mesh | `get_own_mesh_address()`, `discover_networked_thread_devices()`, `discover_mesh_peers()`, `discover_mesh_peers_auto()` |
|
|
63
65
|
| Diagnostics | `get_fsci_diagnostic_info()`, `get_thread_provisioning_info()`, `get_thread_network_state_info()`, `get_scene_diagnostic_info()`, `get_legacy_device_info()`, `get_calibration_extra_info()`, `get_ble_connection_info()`, `get_wifi_remnant_info()`, `get_engineering_diagnostic_info()`, `get_channel_intensities()`, `get_extra_physical_values()` |
|
|
64
66
|
|
|
@@ -88,6 +90,21 @@ Pass `supported_attribute_ids` (read once with
|
|
|
88
90
|
`get_full_poll_batch()` needs the `PrimitiveType` and `Model` from a first
|
|
89
91
|
`get_device_info()`; after that one request per poll is enough.
|
|
90
92
|
|
|
93
|
+
Pass a `ConfigurationCache` (one per device, kept between polls) as
|
|
94
|
+
`get_full_poll_batch(cache=...)` to read the scenes and the schedule only
|
|
95
|
+
when their checksum changed
|
|
96
|
+
([17](./17-configuration-checksums.md#polling-with-the-checksums)).
|
|
97
|
+
`configuration_read` on the result lists what was read, and
|
|
98
|
+
`attribute_table_changed` reports a changed `SupportedAttributesCRC`, after
|
|
99
|
+
which the supported attributes should be read again.
|
|
100
|
+
|
|
101
|
+
Setting the cache's `static_refresh_interval` (seconds) also limits how
|
|
102
|
+
often the attributes that rarely change are read (`STATIC_POLL_ATTRIBUTES`:
|
|
103
|
+
identity, firmware and hardware, color channels, calibration, advanced
|
|
104
|
+
settings, pump flow range and battery settings); in between they come from
|
|
105
|
+
the cache. After writing one of them, call `cache.invalidate_static()`;
|
|
106
|
+
`cache.clear()` makes the next poll read everything.
|
|
107
|
+
|
|
91
108
|
## Relaying
|
|
92
109
|
|
|
93
110
|
```python
|
|
@@ -117,6 +134,9 @@ tank including the gateway itself.
|
|
|
117
134
|
| `light_schedule_to_mob()`, `mob_to_light_schedule()`, `pump_schedule_to_mob()`, `mob_to_pump_schedule()` | `.mob` templates |
|
|
118
135
|
| `dump_attributes()`, `format_attribute_dump_json()`, `parse_attribute_dump()` | Attribute dumps |
|
|
119
136
|
| `format_mesh_address()` | IPv6 text of a mesh address |
|
|
137
|
+
| `is_bluetooth_only()`, `BLUETOOTH_ONLY_MODELS`, `find_bluetooth_only_tank_members()` | Devices of a tank that aren't on its mesh ([09](./09-thread-coap-relay.md#bluetooth-only-devices)) |
|
|
138
|
+
| `supported_attributes_crc()`, `configured_scenes_checksum()`, `schedule_checksum()` | Configuration checksums computed from the data |
|
|
139
|
+
| `posix_tz_string()` | POSIX TZ string of an Olson time zone name |
|
|
120
140
|
|
|
121
141
|
## Command line: `mobius-scan`
|
|
122
142
|
|
|
@@ -138,12 +158,13 @@ With `--by-serial SERIAL` (connects to one device):
|
|
|
138
158
|
|---|---|
|
|
139
159
|
| `--dump-schedule`, `--dump-scenes` | Print the schedule and the scene slots |
|
|
140
160
|
| `--dump-mesh-peers` | Mesh discovery, step by step |
|
|
141
|
-
| `--relay-target SERIAL` | Reach another device through this one; the options below then act on it |
|
|
161
|
+
| `--relay-target SERIAL` | Reach another device through this one; the options below then act on it. A Bluetooth-only device is connected directly instead ([09](./09-thread-coap-relay.md#bluetooth-only-devices)) |
|
|
142
162
|
| `--debug-relay` | Print every layer of each relayed request |
|
|
143
163
|
| `--set-time-to-now`, `--reboot` | Commands |
|
|
144
164
|
| `--set-local-control-enabled`, `--set-auto-dim-timeout`, `--set-max-fan-speed`, `--set-fan-shutdown-enabled` | Advanced features |
|
|
145
165
|
| `--set-battery-backup-speed`, `--set-boosted-battery-power`, `--set-boosted-battery-on-time`, `--set-boosted-battery-off-time` | Pump battery settings |
|
|
146
166
|
| `--dump-attributes FILE` | Attribute dump (JSON) |
|
|
167
|
+
| `--check-checksums` | Compare the device's configuration checksums with their computation ([17](./17-configuration-checksums.md)) |
|
|
147
168
|
| `--dump-schedule-file FILE` / `--load-schedule-file FILE` | Save / load both schedules as editable JSON |
|
|
148
169
|
| `--dump-mob-file FILE` / `--load-mob-file FILE` (`--mob-template-name`, `--mob-which`) | Save / load a `.mob` template |
|
|
149
170
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Configuration checksums
|
|
2
|
+
|
|
3
|
+
A device reports 2-byte checksums of parts of its configuration. Reading a
|
|
4
|
+
checksum instead of the data it covers lets a client re-read the data only
|
|
5
|
+
when it changed.
|
|
6
|
+
|
|
7
|
+
| Attribute | ID | Covers |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `SupportedAttributesCRC` | 14 | The supported attribute table ([03](./03-attributes-and-opcodes.md#listing-supported-attributes)) |
|
|
10
|
+
| `ConfiguredScenesChecksum` | 408 | `ConfiguredScenes` (400) |
|
|
11
|
+
| `Schedule1Checksum` | 501 | `Schedule1` (500) |
|
|
12
|
+
| `Schedule2Checksum` | 504 | `Schedule2` (503) |
|
|
13
|
+
|
|
14
|
+
## SupportedAttributesCRC (14)
|
|
15
|
+
|
|
16
|
+
The app keeps the supported attribute table of every device and computes
|
|
17
|
+
this checksum over it:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
crc16 over, for each attribute in the order the device lists them:
|
|
21
|
+
id (u16) + number of indexes (u8) + each index (u8)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
([CRC16](./02-framing-and-crc.md#crc16), seed 0xFFFF; counts and indexes above
|
|
25
|
+
255 keep only their low byte.) When the device's value differs, the app
|
|
26
|
+
reads the table again, and then every cached feature. The app also reads
|
|
27
|
+
it again after a firmware update. The table is a property of the
|
|
28
|
+
firmware, so the checksum is expected to change only with a firmware
|
|
29
|
+
update. The device's value is the same computation.
|
|
30
|
+
|
|
31
|
+
## ConfiguredScenesChecksum (408)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
crc16 (seed 0xFFFF) over the used slots of ConfiguredScenes (id != 0),
|
|
35
|
+
each slot's bytes as read, in slot order
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Changes when a scene is edited in the app.
|
|
39
|
+
|
|
40
|
+
## Schedule1Checksum (501), Schedule2Checksum (504)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
crc16 (seed 0xFFFF) over the active points of Schedule1 / Schedule2
|
|
44
|
+
(flags bit 0 set), each point's bytes as read, in order
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`Schedule1Intensity` / `Schedule2Intensity` are not included. A schedule
|
|
48
|
+
with no active points (an unused moon schedule, for example) reports
|
|
49
|
+
0xFFFF, the CRC16 of no data.
|
|
50
|
+
|
|
51
|
+
All four computations are confirmed on a VorTech MP40 and a Radion XR15
|
|
52
|
+
G6. `mobius --by-serial SERIAL --check-checksums` compares a device's
|
|
53
|
+
values with them.
|
|
54
|
+
|
|
55
|
+
## Polling with the checksums
|
|
56
|
+
|
|
57
|
+
To read scenes and a schedule only when they changed:
|
|
58
|
+
|
|
59
|
+
1. Read the data together with its checksum in one request, and keep
|
|
60
|
+
both.
|
|
61
|
+
2. On each later poll, read only the checksum (2 bytes). If it equals the
|
|
62
|
+
kept one, the kept data is current.
|
|
63
|
+
3. If it differs, or can't be read, read the data and its checksum again.
|
|
64
|
+
|
|
65
|
+
Read each data attribute in a request of its own: a light's schedule or
|
|
66
|
+
scenes are close to the largest reply a relay carries
|
|
67
|
+
([09](./09-thread-coap-relay.md#reply-size)).
|
|
68
|
+
|
|
69
|
+
The key is the device's own value, so this works without computing
|
|
70
|
+
anything and also catches changes made by other clients (the app, or
|
|
71
|
+
this client's own writes). Include `SupportedAttributesCRC` in the poll: a
|
|
72
|
+
new value means a firmware update, after which the supported attributes
|
|
73
|
+
should be read again.
|
|
74
|
+
|
|
75
|
+
## What the app does with these
|
|
76
|
+
|
|
77
|
+
The app reads none of 408, 501 and 504. It keeps scenes and schedules on
|
|
78
|
+
the phone and reads them again only when it has no copy, has unsaved
|
|
79
|
+
changes of its own, or after the attribute table changed; changes made by
|
|
80
|
+
another client are not noticed.
|
|
81
|
+
|
|
82
|
+
## CRCAttributes (12) and CRCFunction (13)
|
|
83
|
+
|
|
84
|
+
`CRCAttributes` reads as 32 × 0xFFFF on every device seen. The app has
|
|
85
|
+
code that writes a list of attribute IDs (padded to 32 with 0xFFFF) to
|
|
86
|
+
`CRCAttributes` together with a u16 to `CRCFunction`, which looks like a
|
|
87
|
+
device-side check of a checksum over those attributes, but never calls
|
|
88
|
+
it.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-mobius"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.1"
|
|
8
8
|
description = "Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "GPL-2.0-only" }
|
|
@@ -9,6 +9,10 @@ documentation/ for the protocol description.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from .crc import crc16, CRC16_TABLE
|
|
12
|
+
from .checksums import (
|
|
13
|
+
ConfigurationCache, ConfigurationChecksums, configured_scenes_checksum, schedule_checksum,
|
|
14
|
+
supported_attributes_crc,
|
|
15
|
+
)
|
|
12
16
|
from .frame import (
|
|
13
17
|
build_frame, parse_frame, ParsedFrame,
|
|
14
18
|
encode_get_attribute, encode_get_attributes_batch, encode_set_attribute, decode_attribute_response, AttributeValue,
|
|
@@ -22,7 +26,7 @@ from .constants import (
|
|
|
22
26
|
PrimitiveType, PRIMITIVE_SIZE, LIGHT_PRIMITIVES,
|
|
23
27
|
PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL, PUMP_PRIMITIVES, enum_or_none,
|
|
24
28
|
primitive_type_from_name, support_tier, PumpParamRange, pump_param_range, pump_param_errors,
|
|
25
|
-
Model, MODEL_MANUFACTURER, manufacturer_for_model,
|
|
29
|
+
Model, MODEL_MANUFACTURER, manufacturer_for_model, BLUETOOTH_ONLY_MODELS, is_bluetooth_only,
|
|
26
30
|
ErrorState, C2Attribute, OperationMode, PhysicalValueID, VisualID,
|
|
27
31
|
SceneID, OperationState, FsciStatus,
|
|
28
32
|
PumpMode, PumpOverrideMode, RampType, PumpParam, PUMP_PARAM_SIZE, PUMP_MODE_PARAMS,
|
|
@@ -68,9 +72,10 @@ from .device import (
|
|
|
68
72
|
from .discovery import (
|
|
69
73
|
scan_for_mobius_devices, scan_for_mobius_devices_with_info, group_by_pan_id,
|
|
70
74
|
dedupe_by_serial, find_device_by_serial, discover_mesh_peers_via_direct_connect,
|
|
71
|
-
discover_tank,
|
|
75
|
+
discover_tank, find_bluetooth_only_tank_members,
|
|
72
76
|
)
|
|
73
77
|
from .frame import SupportedAttribute
|
|
78
|
+
from .timezone import posix_tz_string, TIME_ZONE_FIELD_SIZE
|
|
74
79
|
from .dump import (
|
|
75
80
|
dump_attributes, format_attribute_dump_json, parse_attribute_dump_json,
|
|
76
81
|
format_attribute_dump_text, parse_attribute_dump_text, parse_attribute_dump,
|
|
@@ -82,12 +87,13 @@ from .mob import (
|
|
|
82
87
|
MAX_PUMP_PRIMITIVE_SIZE,
|
|
83
88
|
)
|
|
84
89
|
|
|
85
|
-
__version__ = "0.
|
|
90
|
+
__version__ = "0.10.1"
|
|
86
91
|
|
|
87
92
|
__all__ = [
|
|
88
93
|
"__version__",
|
|
89
94
|
# crc
|
|
90
|
-
"crc16", "CRC16_TABLE",
|
|
95
|
+
"crc16", "CRC16_TABLE", "supported_attributes_crc", "configured_scenes_checksum",
|
|
96
|
+
"schedule_checksum", "ConfigurationChecksums", "ConfigurationCache",
|
|
91
97
|
# frame
|
|
92
98
|
"build_frame", "parse_frame", "ParsedFrame",
|
|
93
99
|
"encode_get_attribute", "encode_get_attributes_batch", "encode_set_attribute", "decode_attribute_response", "AttributeValue",
|
|
@@ -100,7 +106,7 @@ __all__ = [
|
|
|
100
106
|
"PrimitiveType", "PRIMITIVE_SIZE", "LIGHT_PRIMITIVES",
|
|
101
107
|
"PUMP_PRIMITIVES_VERIFIED", "PUMP_PRIMITIVES_EXPERIMENTAL", "PUMP_PRIMITIVES", "enum_or_none",
|
|
102
108
|
"primitive_type_from_name", "support_tier", "PumpParamRange", "pump_param_range", "pump_param_errors",
|
|
103
|
-
"Model", "MODEL_MANUFACTURER", "manufacturer_for_model",
|
|
109
|
+
"Model", "MODEL_MANUFACTURER", "BLUETOOTH_ONLY_MODELS", "is_bluetooth_only", "manufacturer_for_model",
|
|
104
110
|
"ErrorState", "C2Attribute", "OperationMode", "PhysicalValueID", "VisualID",
|
|
105
111
|
"SceneID", "OperationState", "FsciStatus",
|
|
106
112
|
"PumpMode", "PumpOverrideMode", "RampType", "PumpParam", "PUMP_PARAM_SIZE", "PUMP_MODE_PARAMS",
|
|
@@ -134,7 +140,7 @@ __all__ = [
|
|
|
134
140
|
"next_coap_token",
|
|
135
141
|
"mesh_local_prefix_from_own_address", "mesh_local_prefix_from_mlprefix", "is_valid_mesh_address",
|
|
136
142
|
"build_rloc_address", "extract_short_address", "is_short_address_derived",
|
|
137
|
-
"format_mesh_address",
|
|
143
|
+
"format_mesh_address", "posix_tz_string", "TIME_ZONE_FIELD_SIZE",
|
|
138
144
|
"RelayedMobiusDevice",
|
|
139
145
|
# device
|
|
140
146
|
"MobiusDevice", "MobiusPump", "LightIntensityResult", "LightPollResult", "FullPollResult",
|
|
@@ -143,7 +149,7 @@ __all__ = [
|
|
|
143
149
|
# discovery
|
|
144
150
|
"scan_for_mobius_devices", "scan_for_mobius_devices_with_info", "group_by_pan_id",
|
|
145
151
|
"dedupe_by_serial", "find_device_by_serial", "discover_mesh_peers_via_direct_connect",
|
|
146
|
-
"discover_tank",
|
|
152
|
+
"find_bluetooth_only_tank_members", "discover_tank",
|
|
147
153
|
"SupportedAttribute", "dump_attributes", "enrich_attribute_dump", "generic_interpretations",
|
|
148
154
|
"format_attribute_dump_json", "parse_attribute_dump_json",
|
|
149
155
|
"format_attribute_dump_text", "parse_attribute_dump_text", "parse_attribute_dump",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Checksums a device reports about its own configuration, and how to
|
|
2
|
+
compute them. Reading a 2-byte checksum instead of the data it covers lets
|
|
3
|
+
a client skip re-reading data that hasn't changed.
|
|
4
|
+
|
|
5
|
+
| Attribute | ID | Covers |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `SupportedAttributesCRC` | 14 | The supported attribute table (see supported_attributes_crc()) |
|
|
8
|
+
| `ConfiguredScenesChecksum` | 408 | ConfiguredScenes (400) |
|
|
9
|
+
| `Schedule1Checksum` | 501 | Schedule1 (500) |
|
|
10
|
+
| `Schedule2Checksum` | 504 | Schedule2 (503) |
|
|
11
|
+
|
|
12
|
+
All four are CRC16 with seed 0xFFFF (see the functions below for what each
|
|
13
|
+
covers), confirmed on a VorTech MP40 and a Radion XR15 G6. The app
|
|
14
|
+
computes SupportedAttributesCRC itself and compares it with the device's
|
|
15
|
+
value to decide whether to read the attribute table again; the other
|
|
16
|
+
three it doesn't read.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Iterable, Optional
|
|
24
|
+
|
|
25
|
+
from .crc import crc16
|
|
26
|
+
from .frame import AttributeValue, SupportedAttribute
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def supported_attributes_crc(supported: Iterable[SupportedAttribute]) -> int:
|
|
30
|
+
"""
|
|
31
|
+
The app's checksum of a supported attribute table, for comparison with
|
|
32
|
+
SupportedAttributesCRC (14): crc16 over, for each attribute in the
|
|
33
|
+
order the device lists them, the id (u16 LE), the number of indexes
|
|
34
|
+
(u8) and each index (u8). Counts and indexes above 255 are truncated
|
|
35
|
+
to their low byte, as the app does.
|
|
36
|
+
"""
|
|
37
|
+
data = bytearray()
|
|
38
|
+
for attribute in supported:
|
|
39
|
+
data += struct.pack("<H", attribute.attr_id)
|
|
40
|
+
data.append(len(attribute.indexes) & 0xFF)
|
|
41
|
+
data.extend(index & 0xFF for index in attribute.indexes)
|
|
42
|
+
return crc16(bytes(data))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configured_scenes_checksum(slots: Iterable[bytes]) -> int:
|
|
46
|
+
"""ConfiguredScenesChecksum (408) of ConfiguredScenes (400) slots as
|
|
47
|
+
read: crc16 over the used slots (scene id != 0) in slot order."""
|
|
48
|
+
return crc16(b"".join(slot for slot in slots if len(slot) >= 2 and struct.unpack_from("<H", slot)[0] != 0))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def schedule_checksum(points: Iterable[bytes]) -> int:
|
|
52
|
+
"""Schedule1Checksum (501) / Schedule2Checksum (504) of Schedule1 (500) /
|
|
53
|
+
Schedule2 (503) points as read: crc16 over the active points (flags
|
|
54
|
+
bit 0 set) in order. Schedule1Intensity/Schedule2Intensity are not
|
|
55
|
+
included."""
|
|
56
|
+
return crc16(b"".join(point for point in points if len(point) >= 3 and point[2] & 0x01))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ConfigurationChecksums:
|
|
61
|
+
"""The checksums a device reports; None when not supported or not
|
|
62
|
+
readable."""
|
|
63
|
+
supported_attributes: Optional[int] = None
|
|
64
|
+
configured_scenes: Optional[int] = None
|
|
65
|
+
schedule1: Optional[int] = None
|
|
66
|
+
schedule2: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ConfigurationCache:
|
|
71
|
+
"""
|
|
72
|
+
Data of one device kept between get_full_poll_batch(cache=...) calls
|
|
73
|
+
so a poll reads the 2-byte checksums instead of the data they cover.
|
|
74
|
+
|
|
75
|
+
entries: {data attribute id: (checksum, response blocks)} for
|
|
76
|
+
ConfiguredScenes and the polled schedule, each with the checksum the
|
|
77
|
+
device reported in the same request. A poll whose checksum differs
|
|
78
|
+
(or can't be read) reads the data again. The device's own value is
|
|
79
|
+
the key, so no local computation is involved, and a change by any
|
|
80
|
+
client, this one included, shows up as a changed checksum.
|
|
81
|
+
supported_attributes_crc: SupportedAttributesCRC from the last poll.
|
|
82
|
+
When it changes (a firmware update), the poll result says so
|
|
83
|
+
(FullPollResult.attribute_table_changed) and the static attributes
|
|
84
|
+
are read again on the next poll. `entries` stay valid: each is still
|
|
85
|
+
checked against its own checksum.
|
|
86
|
+
static / static_read_at / static_refresh_interval: attributes that
|
|
87
|
+
rarely change (identity, firmware and hardware, color channels,
|
|
88
|
+
calibration, advanced settings, pump flow range and battery settings;
|
|
89
|
+
STATIC_POLL_ATTRIBUTES in device.py), read at most every
|
|
90
|
+
`static_refresh_interval` seconds and kept here in between. None (the
|
|
91
|
+
default) reads them on every poll. A client that writes one of them
|
|
92
|
+
calls invalidate_static() so the next poll reads them again.
|
|
93
|
+
|
|
94
|
+
One per device.
|
|
95
|
+
"""
|
|
96
|
+
entries: dict[int, tuple[int, list[AttributeValue]]] = field(default_factory=dict)
|
|
97
|
+
supported_attributes_crc: Optional[int] = None
|
|
98
|
+
static: dict[int, list[AttributeValue]] = field(default_factory=dict)
|
|
99
|
+
static_read_at: Optional[float] = None
|
|
100
|
+
static_refresh_interval: Optional[float] = None
|
|
101
|
+
|
|
102
|
+
def static_due(self, now: float) -> bool:
|
|
103
|
+
"""Whether the static attributes are to be read by a poll at
|
|
104
|
+
`now` (time.monotonic())."""
|
|
105
|
+
return (
|
|
106
|
+
self.static_refresh_interval is None or self.static_read_at is None
|
|
107
|
+
or now - self.static_read_at >= self.static_refresh_interval
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def invalidate_static(self) -> None:
|
|
111
|
+
"""Makes the next poll read the static attributes."""
|
|
112
|
+
self.static.clear()
|
|
113
|
+
self.static_read_at = None
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
"""Forgets everything: the next poll reads all data."""
|
|
117
|
+
self.entries.clear()
|
|
118
|
+
self.supported_attributes_crc = None
|
|
119
|
+
self.invalidate_static()
|