python-mobius 0.3.0__tar.gz → 0.4.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.3.0 → python_mobius-0.4.0}/.forgejo/workflows/release.yml +18 -8
- {python_mobius-0.3.0 → python_mobius-0.4.0}/CHANGELOG.md +189 -29
- {python_mobius-0.3.0 → python_mobius-0.4.0}/PKG-INFO +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/README.md +1 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/00-overview.md +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/01-ble-transport.md +1 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/02-framing-and-crc.md +4 -4
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/03-attributes-and-opcodes.md +10 -11
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/04-device-identity.md +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/06-light-schedule.md +51 -13
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/07-pump-schedule.md +3 -4
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/09-thread-coap-relay.md +118 -60
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/11-device-status-attributes.md +15 -21
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/12-device-identity-and-address-stability.md +7 -23
- {python_mobius-0.3.0 → python_mobius-0.4.0}/pyproject.toml +1 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/__init__.py +9 -6
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/cli.py +208 -40
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/coap.py +24 -30
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/constants.py +30 -11
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/crc.py +3 -3
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/device.py +176 -38
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/device_status.py +56 -16
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/discovery.py +61 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/frame.py +1 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/mesh_address.py +32 -12
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/modifiers.py +11 -15
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/power.py +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/pump_status.py +4 -4
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/relay.py +16 -14
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/schedule.py +51 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_coap.py +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_coap_indication_handling.py +4 -3
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_connection_cleanup.py +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_device_time.py +4 -4
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_frame_sniffer.py +2 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_light_intensity_branching.py +1 -1
- python_mobius-0.4.0/tests/test_light_schedule.py +134 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_mesh_discovery.py +109 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_mesh_discovery_direct_connect.py +3 -3
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_modifiers.py +5 -5
- python_mobius-0.4.0/tests/test_networked_thread_devices.py +302 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_power.py +1 -1
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_pump_status.py +2 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_relay.py +3 -2
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_schedule_intensity_scalar.py +2 -2
- python_mobius-0.3.0/tests/test_light_schedule.py +0 -58
- {python_mobius-0.3.0 → python_mobius-0.4.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/.gitignore +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/LICENSE +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/05-scenes.md +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_device_status.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_device_summary.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_frame.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.3.0 → python_mobius-0.4.0}/tests/test_serial_identity.py +0 -0
|
@@ -88,14 +88,23 @@ jobs:
|
|
|
88
88
|
if: github.event_name == 'push'
|
|
89
89
|
run: python3 .forgejo/scripts/extract_changelog_section.py "${{ github.ref_name }}"
|
|
90
90
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
91
|
+
# Both of the action's own README examples always set release-dir
|
|
92
|
+
# explicitly, even though it's documented as optional with a
|
|
93
|
+
# default -- confirmed via a real failure on this exact step
|
|
94
|
+
# ("tag v0.3.1 exists" / "curl: (26) Failed to open/read local
|
|
95
|
+
# data from file/application") that omitting it entirely doesn't
|
|
96
|
+
# work, the same bug already found and fixed the same way for
|
|
97
|
+
# ha-mobius's own release workflow. dist/ has the actual built
|
|
98
|
+
# wheel/sdist in it (from the Build package step above), but
|
|
99
|
+
# those are deliberately NOT what goes here -- PyPI stays the
|
|
100
|
+
# source of truth for the built artifacts, this step only creates
|
|
101
|
+
# the release entry with the extracted changelog section as its
|
|
102
|
+
# notes -- hence a separate, empty dist/release/ rather than
|
|
103
|
+
# pointing release-dir at dist/ itself.
|
|
104
|
+
- name: Ensure release-dir exists (empty -- no files attached)
|
|
105
|
+
if: github.event_name == 'push'
|
|
106
|
+
run: mkdir -p dist/release
|
|
107
|
+
|
|
99
108
|
# Same event-gating reason as the two steps above -- nothing to
|
|
100
109
|
# release against on a manual, non-tag run.
|
|
101
110
|
- name: Create Forgejo release
|
|
@@ -107,4 +116,5 @@ jobs:
|
|
|
107
116
|
repo: r3pek/python-mobius
|
|
108
117
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
109
118
|
tag: ${{ github.ref_name }}
|
|
119
|
+
release-dir: dist/release
|
|
110
120
|
release-notes-file: release_notes.md
|
|
@@ -1,12 +1,171 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- **`mobius-scan`'s default now also connects to ONE device and dumps
|
|
6
|
+
real mesh addresses for its whole network**, not just the pan_id
|
|
7
|
+
listing. Reuses the exact same verbose discovery `--dump-mesh-peers`
|
|
8
|
+
already used (`_debug_mesh_discovery()` -- `MLPrefix`/
|
|
9
|
+
`NetworkedThreadDevices` first, falling back to the older Cowboy-hub
|
|
10
|
+
mechanism), so the output is identical in shape whether it happens via
|
|
11
|
+
the default run or that flag explicitly. `--scan-only` now has real
|
|
12
|
+
meaning again (it had briefly become a no-op after the previous
|
|
13
|
+
change made the pan_id listing itself the whole default): it skips
|
|
14
|
+
this connection step, for a genuinely connection-free run.
|
|
15
|
+
`--dump-tanks` still exists separately for connecting to EVERY pan_id
|
|
16
|
+
group rather than just one, useful when multiple genuinely separate
|
|
17
|
+
tanks/networks are in range at once. Verified end-to-end with the
|
|
18
|
+
scan/connect layer mocked out: confirmed exactly one `MobiusDevice`
|
|
19
|
+
connection gets created by the plain default (to the correct,
|
|
20
|
+
first-found serial), and confirmed `--scan-only` still makes zero.
|
|
21
|
+
|
|
22
|
+
- **Added `Tank` and `discover_tank()`: the connection-based building
|
|
23
|
+
block for full tank discovery** (device + address + age + shared
|
|
24
|
+
prefix, all in one call) -- the "basic block" a downstream integration
|
|
25
|
+
(e.g. ha-mobius) can call to trigger an on-demand scan and show
|
|
26
|
+
address/age/prefix for its devices. `discover_tank(mdevice)` takes an
|
|
27
|
+
already-connected `MobiusDevice`, calls `discover_mesh_peers_auto()`
|
|
28
|
+
plus `get_own_mesh_address()`/`get_device_info()` to include the
|
|
29
|
+
connected device itself in the result (the peer-discovery calls only
|
|
30
|
+
ever report OTHER devices, never themselves) -- so a single-device
|
|
31
|
+
tank correctly comes back with that one device as its sole peer, not
|
|
32
|
+
an empty list that would be indistinguishable from "discovery failed
|
|
33
|
+
entirely." `tank.prefix` is `None` (and `peers` always empty) if the
|
|
34
|
+
connected device didn't report a valid mesh address at all. 3 new
|
|
35
|
+
tests. Also exported `mesh_local_prefix_from_mlprefix()` from the
|
|
36
|
+
top-level `mobius` package -- a real gap from when it was first added,
|
|
37
|
+
caught while adding these new exports alongside it.
|
|
38
|
+
|
|
39
|
+
- **`mobius-scan`'s default behavior changed: now genuinely just scans
|
|
40
|
+
and lists devices grouped by tank, no connections at all** -- it used
|
|
41
|
+
to print that same pan_id-grouped listing (already existed, already
|
|
42
|
+
connection-free) and then STILL go on to connect to every single
|
|
43
|
+
scanned device for a full summary regardless. That's now opt-in via a
|
|
44
|
+
new `--full-summaries` flag instead of being the implicit default.
|
|
45
|
+
Added `--dump-tanks`: connects to just ONE device per pan_id group
|
|
46
|
+
(not every device) and calls the new `discover_tank()` on it, printing
|
|
47
|
+
the full mesh-address/age for every device sharing that tank --
|
|
48
|
+
richer than the default listing, which only has whatever's in the BLE
|
|
49
|
+
advertisement (model/serial), not address or age. `--scan-only` kept
|
|
50
|
+
accepted as a no-op (it's redundant now that its old behavior IS the
|
|
51
|
+
default) rather than removed, so no existing script using it
|
|
52
|
+
explicitly breaks. Renamed the printed header from "Pan ID groups" to
|
|
53
|
+
"Tanks" to match. Verified end-to-end with the scan/connect layer
|
|
54
|
+
mocked out, for both the new default (confirmed zero MobiusDevice
|
|
55
|
+
connections created) and `--dump-tanks` (confirmed it correctly
|
|
56
|
+
prints prefix/address/age for a 2-device tank), not just via
|
|
57
|
+
`--help`'s rendered text.
|
|
58
|
+
|
|
59
|
+
- **`discover_networked_thread_devices()` confirmed against real
|
|
60
|
+
hardware** (a live 4-device tank: 2 VorTech pumps, 2 Radion lights) --
|
|
61
|
+
the byte layout, little-endian field order, and all 4 real peers'
|
|
62
|
+
model/serial/age/derived-address all matched exactly. One real
|
|
63
|
+
correction from this: empty slots on real hardware are encoded as 28
|
|
64
|
+
bytes of raw zero (an all-null-byte serial), **not** the ASCII
|
|
65
|
+
`"00000000000000"` sentinel string `discover_mesh_peers()`'s older
|
|
66
|
+
mechanism uses -- the existing `if not serial` filter already handled
|
|
67
|
+
this correctly (an all-null-byte serial decodes to an empty string
|
|
68
|
+
after stripping), but the docstring incorrectly claimed this newer
|
|
69
|
+
mechanism used the same ASCII convention as the older one. Docstring
|
|
70
|
+
corrected; added a dedicated test for the actual null-byte case, plus
|
|
71
|
+
a new test replaying the exact real capture end-to-end (all 4 peers,
|
|
72
|
+
interleaved with the 26 real empty slots).
|
|
73
|
+
|
|
74
|
+
- **Added `NetworkedThreadDevices` (1001) + `MLPrefix` (2128) support: a
|
|
75
|
+
second, better Thread mesh peer-discovery mechanism.** Confirmed via
|
|
76
|
+
reverse engineering the app's own tank-setup logic to be checked via a
|
|
77
|
+
generic support check on the connected device, **not gated to a
|
|
78
|
+
Cowboy hub** the way the existing three-parallel-arrays mechanism
|
|
79
|
+
(`discover_mesh_peers()`) is -- found while investigating whether the
|
|
80
|
+
app caches device mesh addresses at all (it does, persisting them to
|
|
81
|
+
a local file and syncing them to a cloud backend as part of each
|
|
82
|
+
device's saved state; this attribute is what actually populates that
|
|
83
|
+
cache in the first place). `MobiusDevice.discover_networked_thread_devices()`
|
|
84
|
+
fetches both attributes and returns every peer the connected device
|
|
85
|
+
knows about in a single round-trip, including a per-peer "age" value
|
|
86
|
+
(present in the confirmed wire format, but its exact meaning isn't
|
|
87
|
+
independently confirmed against real hardware -- see `MeshPeer`'s own
|
|
88
|
+
docstring). `MeshPeer` gained an `age: Optional[int] = None` field
|
|
89
|
+
(only ever set by this new method; `None` from every other source, a
|
|
90
|
+
non-breaking addition verified against all 4 existing `MeshPeer(...)`
|
|
91
|
+
construction sites, all of which use keyword arguments). Added
|
|
92
|
+
`mesh_local_prefix_from_mlprefix()` to `mobius.mesh_address`
|
|
93
|
+
(MLPrefix's own response is 24 bytes; only the first 8 are the real
|
|
94
|
+
prefix, confirmed via reverse engineering).
|
|
95
|
+
|
|
96
|
+
- **Added `MobiusDevice.discover_mesh_peers_auto()`: the new
|
|
97
|
+
recommended default entry point for mesh peer discovery**, replacing
|
|
98
|
+
the previous recommendation to call `discover_mesh_peers()` or
|
|
99
|
+
`discover_mesh_peers_via_direct_connect()` directly depending on
|
|
100
|
+
hardware. Tries `discover_networked_thread_devices()` first, falling
|
|
101
|
+
back to `discover_mesh_peers()` only if that finds nothing --
|
|
102
|
+
`discover_mesh_peers_via_direct_connect()` isn't included in this
|
|
103
|
+
chain, since it needs a list of scanned devices as input rather than
|
|
104
|
+
an already-connected `MobiusDevice`; it remains the right fallback to
|
|
105
|
+
reach for explicitly if both of the above come back empty.
|
|
106
|
+
|
|
107
|
+
- **On "discovering Tanks"**: investigated and confirmed not locally
|
|
108
|
+
possible -- the app's own "Tank" grouping is account-level and
|
|
109
|
+
cloud-synced, with user-edited metadata (name, photo) that has no
|
|
110
|
+
local/BLE equivalent to read. The closest local approximation this
|
|
111
|
+
library can offer: every peer `discover_networked_thread_devices()`
|
|
112
|
+
returns shares the connected device's own `MLPrefix`, which in
|
|
113
|
+
practice is "every device on the same Thread mesh" -- for a typical
|
|
114
|
+
setup, the same thing as "every device in the same aquarium."
|
|
115
|
+
Documented as an approximation, not presented as equivalent to the
|
|
116
|
+
app's own Tank object.
|
|
117
|
+
|
|
118
|
+
- **`mobius-scan` CLI updated to match**: `--dump-mesh-peers` and
|
|
119
|
+
`--relay-target` now run the new `discover_mesh_peers_auto()`
|
|
120
|
+
priority order (step 0: `MLPrefix`/`NetworkedThreadDevices`, falling
|
|
121
|
+
through to the existing Cowboy-hub-specific steps only if that finds
|
|
122
|
+
nothing) instead of only ever exercising the older mechanism;
|
|
123
|
+
`--dump-mesh-peers`' output now also prints each peer's `age` when
|
|
124
|
+
set. `--build-peer-map` unchanged in behavior, now documented as the
|
|
125
|
+
last-resort fallback when both attribute-based mechanisms come back
|
|
126
|
+
empty. 10 new tests covering the new discovery method, the prefix
|
|
127
|
+
helper, and both branches of `discover_mesh_peers_auto()`'s fallback
|
|
128
|
+
logic -- plus two direct end-to-end simulations of the updated CLI
|
|
129
|
+
debug path (both the new-mechanism-succeeds case and the
|
|
130
|
+
falls-through-to-the-old-mechanism case) run manually before
|
|
131
|
+
delivery, not just unit-level mocks.
|
|
132
|
+
|
|
133
|
+
## 0.3.1
|
|
134
|
+
|
|
135
|
+
- **Fixed a real, high-impact bug: `Brightness` (the master dimmer
|
|
136
|
+
channel) was never being applied to any other channel at all.**
|
|
137
|
+
Confirmed via reverse engineering the app's own "flatten" logic, called
|
|
138
|
+
on both bracketing schedule points before
|
|
139
|
+
interpolating between them -- an earlier decompilation attempt for this logic
|
|
140
|
+
was unreliable here, the same underlying issue that led to
|
|
141
|
+
finding the `is_night_segment()` bug, so this was re-verified against
|
|
142
|
+
the raw bytecode rather than trusted from the decompile a second time). Every
|
|
143
|
+
channel -- moonlight included -- was being interpolated and returned
|
|
144
|
+
without ever being scaled by `Brightness`, found via real hardware
|
|
145
|
+
testing: a light showing ~1% in the app's own display came back as
|
|
146
|
+
~25% from this library at the same moment, with `Brightness` itself
|
|
147
|
+
reading ~3.3% at that point in its own ramp -- fully consistent with
|
|
148
|
+
the missing multiplication, not a lunar or scheduling bug.
|
|
149
|
+
`interpolate_light_schedule()` now flattens each bracketing point
|
|
150
|
+
SEPARATELY (by that point's own `Brightness` value) BEFORE
|
|
151
|
+
interpolating between the two already-flattened points -- confirmed
|
|
152
|
+
via a dedicated test that this is NOT mathematically equivalent to
|
|
153
|
+
interpolating raw values first and multiplying by a single
|
|
154
|
+
interpolated `Brightness` at the end, whenever `Brightness` itself
|
|
155
|
+
changes across the segment (which it usually does, e.g. ramping
|
|
156
|
+
during dusk/dawn). `Brightness` itself is still returned as the plain
|
|
157
|
+
raw interpolated value, not flattened to a constant 100% the way the
|
|
158
|
+
real `flatten()` does to the primitive it's applied to -- callers
|
|
159
|
+
(e.g. a dedicated `Brightness` sensor) want the actual current
|
|
160
|
+
master-dimmer level. 4 new tests.
|
|
161
|
+
|
|
3
162
|
## 0.3.0
|
|
4
163
|
|
|
5
164
|
- **Fixed a real, high-impact bug in `is_night_segment()`: the condition
|
|
6
165
|
deciding whether to apply lunar-phase reduction had its first check
|
|
7
|
-
inverted**, confirmed directly
|
|
8
|
-
|
|
9
|
-
method
|
|
166
|
+
inverted**, confirmed directly against the raw bytecode after an
|
|
167
|
+
earlier decompilation attempt on this exact
|
|
168
|
+
method turned out to be unreliable -- a real signal, not noise: the decompiler had
|
|
10
169
|
produced `!point.has(NIGHT)` where the actual bytecode requires
|
|
11
170
|
`point.has(NIGHT)`. In practice this meant lunar reduction was applied
|
|
12
171
|
only during the single dusk-transition segment (and only if the query
|
|
@@ -87,16 +246,16 @@ for the full protocol trace and implementation history. The `coap-relay`
|
|
|
87
246
|
branch has been merged into main as part of this release.
|
|
88
247
|
|
|
89
248
|
- **Root cause of relayed reads returning empty data found and fixed,**
|
|
90
|
-
after the user provided the raw
|
|
91
|
-
|
|
92
|
-
|
|
249
|
+
after the user provided the raw disassembly for
|
|
250
|
+
the app's own connection logic -- the decompiler had given up specifically on
|
|
251
|
+
the indication-handling logic, the one thing that held the answer, but the
|
|
93
252
|
underlying bytecode was still traceable. Confirmed precisely: the real
|
|
94
253
|
CoAP relay response arrives on a completely separate opcode (`0xDF`/
|
|
95
254
|
`26`, `COAP_INDICATION_OPCODE` -- not the request's `25`, whose confirm
|
|
96
255
|
is a bare, meaningless ack the app itself discards, confirmed
|
|
97
256
|
byte-for-byte via real hardware testing showing exactly 1 byte in every
|
|
98
257
|
relayed read's outer response), matched purely by CoAP token
|
|
99
|
-
(
|
|
258
|
+
(confirmed via reverse engineering: `coapRequest.getToken() ==
|
|
100
259
|
coapResponse.getToken()`), never by the outer FSCI message ID. Added
|
|
101
260
|
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` -- a
|
|
102
261
|
token-keyed pending-response registry, entirely separate from the
|
|
@@ -120,7 +279,8 @@ branch has been merged into main as part of this release.
|
|
|
120
279
|
even though that same device's own address reads back fine). Added
|
|
121
280
|
`MobiusDevice.get_own_mesh_address()` and `mobius.discovery.
|
|
122
281
|
discover_mesh_peers_via_direct_connect()` as the alternative --
|
|
123
|
-
confirmed via
|
|
282
|
+
confirmed via reverse engineering the app's own connection-setup logic
|
|
283
|
+
to be exactly how the app
|
|
124
284
|
itself always learns a device's address (automatic on every
|
|
125
285
|
connection, not Cowboy-specific). Added `mobius-scan --build-peer-map`
|
|
126
286
|
to exercise this from the CLI. `discover_mesh_peers()` remains in
|
|
@@ -150,15 +310,15 @@ branch has been merged into main as part of this release.
|
|
|
150
310
|
|
|
151
311
|
- **Confirmed directly from source why the response comes back
|
|
152
312
|
malformed: the immediate confirm to a relay request is explicitly
|
|
153
|
-
discarded by the app itself, on purpose.**
|
|
154
|
-
|
|
313
|
+
discarded by the app itself, on purpose.** The app's own byte-dispatch
|
|
314
|
+
logic matches `opGroup==0xDF && opCode==25` unconditionally
|
|
155
315
|
and logs it only -- before the dispatcher ever checks whether it
|
|
156
316
|
resolves a pending request. Real hardware testing independently
|
|
157
317
|
confirmed this byte-for-byte: every relayed read's outer response is
|
|
158
318
|
exactly 1 byte (`0x00`), not a truncated response but the genuine
|
|
159
319
|
complete confirm, thrown away by the app too. The real response, if it
|
|
160
320
|
arrives at all, comes through a separate "indication" handled by
|
|
161
|
-
|
|
321
|
+
the app's own indication-handling logic -- a method the decompiler couldn't resolve to readable code, a genuine
|
|
162
322
|
limit of what source tracing alone can determine here. Added
|
|
163
323
|
`MobiusDevice.on_any_frame` (fires for every parsed incoming frame,
|
|
164
324
|
not just ones resolving a pending request) and extended
|
|
@@ -169,8 +329,8 @@ branch has been merged into main as part of this release.
|
|
|
169
329
|
- **Added `mobius.coap`**: CoAP (RFC 7252) protocol primitives --
|
|
170
330
|
`CoapRequestType`/`CoapMethod`/`CoapResponseCode` enums,
|
|
171
331
|
`encode_coap_request()`/`decode_coap_response()`. Pure functions, no
|
|
172
|
-
hardware dependency. Confirmed wire format traced directly from
|
|
173
|
-
|
|
332
|
+
hardware dependency. Confirmed wire format traced directly from
|
|
333
|
+
reverse engineering the app's own CoAP model --
|
|
174
334
|
see the module's docstring for exactly which enum values are
|
|
175
335
|
confirmed-literal, cross-referenced from elsewhere in this project, or
|
|
176
336
|
inferred-only (deliberately not guessing at unconfirmed ones -- an
|
|
@@ -184,8 +344,8 @@ branch has been merged into main as part of this release.
|
|
|
184
344
|
`mesh_local_prefix_from_own_address()`/`is_valid_mesh_address()`/
|
|
185
345
|
`is_short_address_derived()`). Pure functions, no hardware dependency.
|
|
186
346
|
Includes a genuinely surprising, verified-not-assumed finding: the
|
|
187
|
-
short-address portion is little-endian (confirmed via
|
|
188
|
-
|
|
347
|
+
short-address portion is little-endian (confirmed via reverse
|
|
348
|
+
engineering the app's own byte-extraction logic), the
|
|
189
349
|
opposite of IPv6's own conventional big-endian byte order -- easy to
|
|
190
350
|
get backwards by assuming the IPv6 standard's own convention applies
|
|
191
351
|
here too. 15 new tests.
|
|
@@ -199,8 +359,8 @@ branch has been merged into main as part of this release.
|
|
|
199
359
|
per-element format matches the existing 14-byte ASCII serial format
|
|
200
360
|
already used elsewhere in this library -- no new parsing logic needed.
|
|
201
361
|
Filters out cleared/empty slots (the literal sentinel string
|
|
202
|
-
`"00000000000000"`, confirmed via
|
|
203
|
-
cleanup logic). Fails soft to an empty list for unsupported devices or
|
|
362
|
+
`"00000000000000"`, confirmed via reverse engineering the app's own
|
|
363
|
+
network-setup cleanup logic). Fails soft to an empty list for unsupported devices or
|
|
204
364
|
ones not currently part of an active Thread network, matching this
|
|
205
365
|
library's usual convention. Also recorded (but not yet used) a fourth
|
|
206
366
|
parallel array, `BladeBleAddressArray` (3714, each peer's real BLE MAC
|
|
@@ -249,8 +409,8 @@ branch has been merged into main as part of this release.
|
|
|
249
409
|
|
|
250
410
|
- **Added `get_device_time_info()`** (`Epoch`/`RTCTime`, attributes
|
|
251
411
|
201/219) -- device clock drift vs. local system time. Not speculative:
|
|
252
|
-
confirmed via the app's own "Time Difference" troubleshooting feature
|
|
253
|
-
|
|
412
|
+
confirmed via the app's own "Time Difference" troubleshooting feature,
|
|
413
|
+
which checks these exact two attributes
|
|
254
414
|
the same way. Included automatically in `get_device_summary()`'s
|
|
255
415
|
`"device_time"` field. Important caveat documented: the app's own
|
|
256
416
|
schedule interpolation uses the phone/tank clock, not either of these,
|
|
@@ -371,8 +531,7 @@ branch has been merged into main as part of this release.
|
|
|
371
531
|
(`IsCalibrated`/`LastCalibrationTime`/`MinCalibratedSpeed`/
|
|
372
532
|
`MaxCalibratedSpeed`, 1300/1304/1308/1309) -- **important correction**:
|
|
373
533
|
despite "Speed"-sounding names, confirmed via the app's own UI gating
|
|
374
|
-
(
|
|
375
|
-
M.DeviceCategory.Lighting`) that this is a light feature, not a pump
|
|
534
|
+
(its own device category check) that this is a light feature, not a pump
|
|
376
535
|
one. A separate, unrelated pump-side calibration flow exists in the app
|
|
377
536
|
(Nero-specific zero-flow/max-flow calibration) but isn't implemented and
|
|
378
537
|
doesn't apply to VorTech anyway (no physical flow sensor to calibrate).
|
|
@@ -401,7 +560,7 @@ branch has been merged into main as part of this release.
|
|
|
401
560
|
- **Added full hyperdrive support** (correct percentage conversion for
|
|
402
561
|
light channels boosted above their normal 100% rating). New
|
|
403
562
|
`mobius.power` module (`ChannelPowerInfo`, `channel_percent_value()`,
|
|
404
|
-
|
|
563
|
+
confirmed via reverse engineering the app's own power model) and `get_channel_power_info()`
|
|
405
564
|
(`MaxPower`/`NormalPower`, attributes 1504/1513). New
|
|
406
565
|
`get_current_light_percentages()` convenience method returns corrected
|
|
407
566
|
0-100+ percentages instead of raw permille, flagging boosted channels via
|
|
@@ -434,20 +593,21 @@ branch has been merged into main as part of this release.
|
|
|
434
593
|
a schedule-level master intensity scalar (`Schedule1Intensity`, attribute
|
|
435
594
|
511) that the real app applies on top. Caught by direct comparison
|
|
436
595
|
against real app screenshots showing different numbers than this
|
|
437
|
-
library computed for the same light at the same time. Traced
|
|
438
|
-
|
|
596
|
+
library computed for the same light at the same time. Traced through
|
|
597
|
+
reverse engineering the app's own dashboard-rendering logic.
|
|
439
598
|
Added `get_schedule_intensity()`; `get_current_light_intensities()` now
|
|
440
599
|
applies it automatically.
|
|
441
600
|
- **Added lunar-phase and acclimation-ramp support**, the two remaining
|
|
442
601
|
modifiers flagged as gaps in the previous entry. New `mobius.modifiers`
|
|
443
|
-
module,
|
|
602
|
+
module, confirmed via reverse engineering the app's own lunar and
|
|
603
|
+
acclimation models:
|
|
444
604
|
- `lunar_percent_reduction(date)` -- verified against real,
|
|
445
605
|
independently-confirmed 2026 moon phase dates (full moon Jan 3, new
|
|
446
606
|
moon Jan 18); the ported algorithm matches exactly.
|
|
447
607
|
- `AcclimationInfo` -- linear intensity ramp from a starting percentage
|
|
448
608
|
up to full over a configured period.
|
|
449
|
-
- `is_night_segment()` --
|
|
450
|
-
|
|
609
|
+
- `is_night_segment()` -- confirmed via reverse engineering the boolean half of
|
|
610
|
+
the app's own schedule-interpolation logic; confirmed this is specifically
|
|
451
611
|
"the dusk-to-night transition segment," not simply "is it night."
|
|
452
612
|
- `get_lunar_enabled()`, `get_acclimation_info()`,
|
|
453
613
|
`get_current_light_intensities()` now applies the full branching the
|
|
@@ -462,8 +622,8 @@ branch has been merged into main as part of this release.
|
|
|
462
622
|
|
|
463
623
|
## 0.1.2
|
|
464
624
|
|
|
465
|
-
- Added `manufacturer_for_model()` / `MODEL_MANUFACTURER` (
|
|
466
|
-
|
|
625
|
+
- Added `manufacturer_for_model()` / `MODEL_MANUFACTURER` (confirmed via
|
|
626
|
+
reverse engineering the app's own manufacturer-lookup logic) — surfaces "EcoTech Marine" /
|
|
467
627
|
"AquaIllumination" / "Neptune Systems" / "NYOS" per device model. Now
|
|
468
628
|
included automatically in `get_device_info()`'s `"manufacturer"` field.
|
|
469
629
|
Added while building a Home Assistant integration on top of this library,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -140,7 +140,7 @@ GPLv2 — see [`LICENSE`](./LICENSE).
|
|
|
140
140
|
|
|
141
141
|
The protocol reverse-engineering and implementation in this library were
|
|
142
142
|
carried out with substantial assistance from Claude (Anthropic), used to
|
|
143
|
-
analyze a decompiled copy of the official Mobius Android app
|
|
143
|
+
analyze a decompiled copy of the official Mobius Android app and
|
|
144
144
|
cross-reference it against prior public community research (notably the
|
|
145
145
|
Reef2Reef "Controlling Mobius enabled VorTech pump using 0-10V and BLE"
|
|
146
146
|
thread and the `danmrossi/MobiusControl` project), then to design, write,
|
|
@@ -108,7 +108,7 @@ GPLv2 — see [`LICENSE`](./LICENSE).
|
|
|
108
108
|
|
|
109
109
|
The protocol reverse-engineering and implementation in this library were
|
|
110
110
|
carried out with substantial assistance from Claude (Anthropic), used to
|
|
111
|
-
analyze a decompiled copy of the official Mobius Android app
|
|
111
|
+
analyze a decompiled copy of the official Mobius Android app and
|
|
112
112
|
cross-reference it against prior public community research (notably the
|
|
113
113
|
Reef2Reef "Controlling Mobius enabled VorTech pump using 0-10V and BLE"
|
|
114
114
|
thread and the `danmrossi/MobiusControl` project), then to design, write,
|
|
@@ -15,7 +15,7 @@ Interface conventions, extended by EcoTech).
|
|
|
15
15
|
sniffing and partial decompilation. The `danmrossi/MobiusControl` GitHub
|
|
16
16
|
repo has a working Arduino/ESP32 implementation built on that research.
|
|
17
17
|
|
|
18
|
-
2. **Direct
|
|
18
|
+
2. **Direct reverse engineering of the Mobius Android app**, which
|
|
19
19
|
confirmed everything from (1) and filled in every gap it left:
|
|
20
20
|
- The exact reserved-byte bit layout.
|
|
21
21
|
- Both the "old" (1-byte field) and "new" (2-byte field) opcode variants
|
|
@@ -60,7 +60,7 @@ The decompiled-source analysis, protocol derivation, and Python
|
|
|
60
60
|
implementation in this project were done with substantial assistance from
|
|
61
61
|
Claude (Anthropic) — reading the decompiled Java sources, cross-referencing
|
|
62
62
|
them against the public community research above, resolving several
|
|
63
|
-
|
|
63
|
+
decompilation artifacts specific to the reverse-engineering process (see
|
|
64
64
|
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md) for a
|
|
65
65
|
specific example), and writing/testing the resulting Python code. Every
|
|
66
66
|
claim in these docs is still tagged by how it was actually confirmed (see
|
|
@@ -16,7 +16,7 @@ This UUID pattern is a generic Nordic-style "wireless UART" shape; the
|
|
|
16
16
|
`01ff01xx` prefix is what actually identifies it as Mobius-specific, not the
|
|
17
17
|
UUID base itself.
|
|
18
18
|
|
|
19
|
-
**Confirmed** via the
|
|
19
|
+
**Confirmed** via reverse engineering the app's own connection logic: the app writes
|
|
20
20
|
to `TX_DATA` for every chunk except the last, and to `TX_FINAL` for the
|
|
21
21
|
last (or only) chunk — writing to `TX_FINAL` is what triggers the device to
|
|
22
22
|
actually process the accumulated message. Symmetrically, on receive, it
|
|
@@ -44,7 +44,7 @@ explored.
|
|
|
44
44
|
|
|
45
45
|
## `reserved` field bit layout
|
|
46
46
|
|
|
47
|
-
Confirmed
|
|
47
|
+
Confirmed via reverse engineering:
|
|
48
48
|
|
|
49
49
|
```
|
|
50
50
|
byte0: bits 0-1 = FSCI instance
|
|
@@ -66,10 +66,10 @@ def crc16(data: bytes, seed: int = 0xFFFF) -> int:
|
|
|
66
66
|
return crc
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
The 256-entry table is
|
|
70
|
-
|
|
69
|
+
The 256-entry table is confirmed via reverse engineering the app's own
|
|
70
|
+
CRC implementation (the original signed-short constants, converted
|
|
71
71
|
to unsigned 16-bit for Python — see `src/mobius/crc.py`). There's also an
|
|
72
|
-
unused CRC8 table
|
|
72
|
+
unused CRC8 table alongside it, not used by anything observed and
|
|
73
73
|
not ported here.
|
|
74
74
|
|
|
75
75
|
## Message ID / request-response correlation
|
|
@@ -46,7 +46,7 @@ from a multi-element Get, this assumption is the first thing to suspect.
|
|
|
46
46
|
|
|
47
47
|
## The `C2Attribute` table
|
|
48
48
|
|
|
49
|
-
The
|
|
49
|
+
The app's own attribute-identifier enum has on the order of 250 entries,
|
|
50
50
|
covering the entire equipment line (lighting, pumps, dosing, Thread
|
|
51
51
|
networking, OTA firmware, engineering/debug attributes, etc.). This library
|
|
52
52
|
implements a curated subset — see `src/mobius/constants.py::C2Attribute` —
|
|
@@ -54,23 +54,22 @@ covering device identity, scenes, schedules, pump telemetry, and light
|
|
|
54
54
|
channels.
|
|
55
55
|
|
|
56
56
|
Every value is either a literal read directly from the decompile, or
|
|
57
|
-
resolved through a
|
|
57
|
+
resolved through a symbolic-substitution artifact (explained below)
|
|
58
58
|
back to a real integer — confirmed, not guessed, in both cases.
|
|
59
59
|
|
|
60
|
-
### The
|
|
60
|
+
### The symbolic-substitution artifact
|
|
61
61
|
|
|
62
|
-
R8/ProGuard inlines small integer constants at compile time.
|
|
63
|
-
|
|
62
|
+
R8/ProGuard inlines small integer constants at compile time. The
|
|
63
|
+
decompiler, in turn, sometimes picks a symbolic name for one of these literals from
|
|
64
64
|
an *unrelated* imported class whose constant happens to share the same
|
|
65
65
|
numeric value — purely cosmetic, but it means the decompiled source shows
|
|
66
|
-
|
|
66
|
+
a symbolic name from an unrelated library instead of the literal `401`.
|
|
67
67
|
|
|
68
68
|
Most of these were resolved by obtaining the real source of the referenced
|
|
69
|
-
library class
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
confirmed `ERROR_UNKNOWN = 500`, and therefore `Schedule1 = 500`. This was
|
|
69
|
+
library class and reading its actual constant values. One, `Schedule1`, required an extra
|
|
70
|
+
hop: it resolved to an unrelated library's internal
|
|
71
|
+
constant — obtaining *that* class's source
|
|
72
|
+
confirmed the real value was `500`, and therefore `Schedule1 = 500`. This was
|
|
74
73
|
subsequently confirmed live: fetching schedules with attribute ID 500
|
|
75
74
|
against real pumps and lights returned correctly-structured, sane data.
|
|
76
75
|
|
|
@@ -58,7 +58,7 @@ a fourth "Hera" product line (codenamed after Taco Bell menu items in the
|
|
|
58
58
|
source — `CrunchyTaco`, `SoftTaco`, `BajaBlast`, etc.). This library
|
|
59
59
|
implements a curated subset relevant to lights/pumps/skimmers — see
|
|
60
60
|
`src/mobius/constants.py::Model`; add entries as needed by copying values
|
|
61
|
-
from the
|
|
61
|
+
from the app's own model-identifier enum.
|
|
62
62
|
|
|
63
63
|
Some newer model names in the decompile are Base64-encoded string literals
|
|
64
64
|
rather than plain strings (likely an unrelated obfuscation pass) — e.g.
|
|
@@ -66,7 +66,7 @@ model `179` decodes to `"Radion XR15w G6 Pro"` from
|
|
|
66
66
|
`base64.b64decode("UmFkaW9uIFhSMTV3IEc2IFBybw==")`. `RadionXR15wG6Blue = 180`
|
|
67
67
|
is a sequential-numbering *inference*, not a directly-read literal (it sits
|
|
68
68
|
in a gap between two confirmed values, but the exact literal wasn't
|
|
69
|
-
resolvable through the
|
|
69
|
+
resolvable through the symbolic-substitution technique the way others were).
|
|
70
70
|
|
|
71
71
|
## `ErrorState`
|
|
72
72
|
|
|
@@ -17,8 +17,7 @@ lunar/acclimation modifiers.
|
|
|
17
17
|
|
|
18
18
|
## Schedule-level master intensity (`Schedule1Intensity`, attribute 511)
|
|
19
19
|
|
|
20
|
-
Confirmed via
|
|
21
|
-
app: on top of the per-channel interpolated curve, the app applies a
|
|
20
|
+
Confirmed via reverse engineering the app's own dashboard-rendering logic: on top of the per-channel interpolated curve, the app applies a
|
|
22
21
|
**separate schedule-level master dimmer** -- `dCurrentIntensity =
|
|
23
22
|
schedule.intensity / 1000.0`, multiplied onto every channel's interpolated
|
|
24
23
|
value. This is genuinely separate from any individual channel's own value,
|
|
@@ -75,18 +74,57 @@ lights.
|
|
|
75
74
|
|
|
76
75
|
## Interpolation algorithm
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
Confirmed via reverse engineering the app's own schedule-interpolation logic:
|
|
79
78
|
|
|
80
79
|
1. Sort points by time.
|
|
81
80
|
2. Find the two points that bracket the target minute-of-day, wrapping
|
|
82
81
|
around midnight (the point after the last one is treated as being on
|
|
83
82
|
"the next day," i.e. its time + 1440).
|
|
84
|
-
3.
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
3. **`Brightness` (`VisualID.Brightness`) is a master dimmer, applied to
|
|
84
|
+
EACH bracketing point separately, before interpolating between them**
|
|
85
|
+
-- confirmed via reverse engineering the app's own "flatten" logic, called on
|
|
86
|
+
both point copies before the
|
|
87
|
+
interpolation loop -- an earlier decompilation attempt for this logic
|
|
88
|
+
was unreliable here, the same underlying issue that led to
|
|
89
|
+
finding the `is_night_segment()` bug, so this was verified against
|
|
90
|
+
the raw bytecode directly rather than trusted from the decompile). For each
|
|
91
|
+
point independently: every channel except `Brightness` itself is
|
|
92
|
+
multiplied by that same point's own `Brightness` value / 1000. This
|
|
93
|
+
was a real, confirmed-missing step for a while -- every channel
|
|
94
|
+
(moonlight included) was being interpolated and returned without
|
|
95
|
+
ever being scaled by `Brightness` at all, found via real hardware
|
|
96
|
+
testing (a light showing ~1% in the app's own display came back as
|
|
97
|
+
~25% from this library at the same moment, with `Brightness` itself
|
|
98
|
+
reading ~3.3% at that point in its own ramp).
|
|
99
|
+
4. For each channel present in either bracketing point, linearly
|
|
100
|
+
interpolate between the (already `Brightness`-scaled, for
|
|
101
|
+
non-`Brightness` channels) endpoint values:
|
|
102
|
+
`value(t) = v1 + (v2 - v1) * (t - t1) / (t2 - t1)`.
|
|
103
|
+
**Flattening happens per-endpoint before this step, not
|
|
104
|
+
interpolate-raw-then-multiply-by-a-single-`Brightness`-value after
|
|
105
|
+
it** -- these are NOT mathematically equivalent whenever `Brightness`
|
|
106
|
+
itself changes across the segment (which it usually does, e.g.
|
|
107
|
+
ramping during a dusk/dawn transition).
|
|
108
|
+
5. Channels absent from a point default to `0` (off), except
|
|
109
|
+
`Brightness` itself: a point missing that channel entirely is treated
|
|
110
|
+
as a no-op 1000 (100%) multiplier for that point, matching
|
|
111
|
+
`flatten()`'s own `visuals().contains(Brightness)` guard (skips
|
|
112
|
+
scaling entirely rather than treating an absent value as 0, which
|
|
113
|
+
would zero out every other channel).
|
|
114
|
+
6. `Brightness` itself is returned as the plain raw interpolated value
|
|
115
|
+
between the two points' own `Brightness` levels -- NOT flattened to
|
|
116
|
+
1000 the way the real `flatten()` call does internally to the
|
|
117
|
+
primitive it's applied to. Unlike the app's own dashboard (which
|
|
118
|
+
doesn't display `Brightness` as its own value at all, only as the
|
|
119
|
+
multiplier baked into every other channel), this library's callers
|
|
120
|
+
(e.g. a dedicated `Brightness` sensor) want the actual current
|
|
121
|
+
master-dimmer level itself.
|
|
87
122
|
|
|
88
123
|
Verified against a synthetic two-point schedule (exact endpoint values,
|
|
89
|
-
midpoint blending,
|
|
124
|
+
midpoint blending, midnight-wraparound blending, and the `Brightness`
|
|
125
|
+
master-dimmer behavior above, specifically including a case designed to
|
|
126
|
+
mathematically distinguish flatten-then-interpolate from the wrong
|
|
127
|
+
interpolate-then-multiply alternative -- see
|
|
90
128
|
`tests/test_light_schedule.py`) and against a real 9-point schedule
|
|
91
129
|
fetched live, which produces a coherent daily light-intensity curve (dim
|
|
92
130
|
moonlight overnight → ramp to full blues/UV midday → wind back down).
|
|
@@ -100,7 +138,7 @@ A static list of `VisualID` values (channel identities) a given light has
|
|
|
100
138
|
## Lunar phase and acclimation ramp
|
|
101
139
|
|
|
102
140
|
Both implemented, on top of the schedule-level master intensity above.
|
|
103
|
-
|
|
141
|
+
Confirmed via reverse engineering the app's own lunar and acclimation models.
|
|
104
142
|
|
|
105
143
|
**Lunar phase** (`lunar_percent_reduction(date)`): a simplified moon-phase
|
|
106
144
|
model -- a Julian-Day-based calculation (`julian_day_from_date()`, the
|
|
@@ -116,8 +154,8 @@ where `is_night_segment()` is true (see below), and only when
|
|
|
116
154
|
that segment just uses a flat `1.0` (full/unreduced) scalar, matching
|
|
117
155
|
the app's own fallback.
|
|
118
156
|
|
|
119
|
-
**`is_night_segment()`**:
|
|
120
|
-
|
|
157
|
+
**`is_night_segment()`**: confirmed via reverse engineering the boolean half of
|
|
158
|
+
the app's own schedule-interpolation return value. This is *not*
|
|
121
159
|
simply "is it night" as a general concept -- it's specifically true for
|
|
122
160
|
the segment whose **start** point IS flagged `NIGHT` (but not also
|
|
123
161
|
`SUNRISE`) and whose end point is ALSO flagged `NIGHT` -- i.e. once
|
|
@@ -127,9 +165,9 @@ point. In practice this covers essentially the whole night, *except*
|
|
|
127
165
|
the two transition segments themselves: dusk (start point not yet
|
|
128
166
|
night-flagged) and the sunrise segment (start point flagged `SUNRISE`).
|
|
129
167
|
|
|
130
|
-
**Confirmed directly
|
|
131
|
-
|
|
132
|
-
exact method -- a real signal, not noise:
|
|
168
|
+
**Confirmed directly against the raw bytecode, not the decompiled Java.**
|
|
169
|
+
An earlier decompilation attempt for this
|
|
170
|
+
exact method was unreliable -- a real signal, not noise: the decompiled Java had this
|
|
133
171
|
condition's first check inverted (`!point.has(NIGHT)` instead of the
|
|
134
172
|
actual `point.has(NIGHT)`), which an earlier version of this port
|
|
135
173
|
carried forward uncorrected. Found via real hardware testing: a light
|
|
@@ -81,11 +81,10 @@ byte):
|
|
|
81
81
|
`Time`, `StartTime`, and `EndTime` (**only these three** — not `OnTime`/
|
|
82
82
|
`OffTime`/`BigTime`, which are stored raw) are stored as
|
|
83
83
|
`actual_value - 250` on the wire, and the app adds `250` back when reading.
|
|
84
|
-
`250` is a real Android constant
|
|
85
|
-
|
|
86
|
-
— another instance of the JADX cosmetic-substitution artifact described in
|
|
84
|
+
`250` is a real Android library constant
|
|
85
|
+
— another instance of the symbolic-substitution artifact described in
|
|
87
86
|
[03-attributes-and-opcodes.md](./03-attributes-and-opcodes.md), resolved by
|
|
88
|
-
looking up that
|
|
87
|
+
looking up that library constant's real value. Verified against
|
|
89
88
|
synthetic round-trip data (`tests/test_pump_schedule.py`).
|
|
90
89
|
|
|
91
90
|
### `MaxSpeed`/`MinSpeed` sign
|