-
released this
2026-08-18 11:36:26 +01:00 | 12 commits to main since this release- Filled in the rest of the device model list (EcoTech Marine,
AquaIllumination, Neptune Systems, NYOS), previously a curated
subset. Also corrects Model.VersaVX1, mismarked as AquaIllumination;
it's an EcoTech Marine product. - Several Model entries are now named for their real product (e.g.
Model.Axis40, Model.Blade, Model.Quantum160) instead of the app's
own internal placeholder codename (Turtle2, Saddle, Coffee1). - Fixed the lunar-phase-scaled light intensity sometimes disagreeing
with the app for hours at a stretch. The lunar calculation now uses
the gateway device's own reported date instead of the local
system's, matching what the app itself actually does.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Filled in the rest of the device model list (EcoTech Marine,
-
released this
2026-08-14 09:38:09 +01:00 | 18 commits to main since this release- Fixed relayed devices (anything reached through a gateway over the
Thread mesh, not directly over BLE) occasionally losing a read for
no apparent reason, even though the official app kept working fine.
A single relay round-trip crosses both BLE and the Thread mesh, and
one dropped or delayed packet somewhere in that longer path turned
out to be common enough that the app always retries once before
giving up -- this library now does the same.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Fixed relayed devices (anything reached through a gateway over the
-
released this
2026-08-13 14:46:18 +01:00 | 20 commits to main since this release- Fixed
MeshPeer.agebeing parsed as a signed number instead of
unsigned -- could have shown as negative for a peer that had been
unreachable a very long time. Also confirmed what this value
actually means: milliseconds since that peer was last heard from on
the mesh, not since it was first found. It resets whenever the mesh
hears from that device again, so seeing it go down between two
checks is expected, not a bug. - Fixed
mobius-scan --dump-schedulecrashing for pumps specifically
('PumpSchedulePoint' object has no attribute 'has') -- that class
was missing a methodSchedulePoint(the light-schedule equivalent)
already had, despite sharing the exact same flag bits.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Fixed
-
released this
2026-08-08 14:40:02 +01:00 | 24 commits to main since this release- Fixed
discover_tank()sometimes reporting the connected device
twice in a tank's peer list.
Downloads
-
Source code (ZIP)
2 downloads
-
Source code (TAR.GZ)
2 downloads
- Fixed
-
released this
2026-08-07 23:29:27 +01:00 | 27 commits to main since this release-
mobius-scan's default now also connects to ONE device and dumps
real mesh addresses for its whole network, not just the pan_id
listing. Reuses the exact same verbose discovery--dump-mesh-peers
already used (_debug_mesh_discovery()--MLPrefix/
NetworkedThreadDevicesfirst, falling back to the older Cowboy-hub
mechanism), so the output is identical in shape whether it happens via
the default run or that flag explicitly.--scan-onlynow has real
meaning again (it had briefly become a no-op after the previous
change made the pan_id listing itself the whole default): it skips
this connection step, for a genuinely connection-free run.
--dump-tanksstill exists separately for connecting to EVERY pan_id
group rather than just one, useful when multiple genuinely separate
tanks/networks are in range at once. Verified end-to-end with the
scan/connect layer mocked out: confirmed exactly oneMobiusDevice
connection gets created by the plain default (to the correct,
first-found serial), and confirmed--scan-onlystill makes zero. -
Added
Tankanddiscover_tank(): the connection-based building
block for full tank discovery (device + address + age + shared
prefix, all in one call) -- the "basic block" a downstream integration
(e.g. ha-mobius) can call to trigger an on-demand scan and show
address/age/prefix for its devices.discover_tank(mdevice)takes an
already-connectedMobiusDevice, callsdiscover_mesh_peers_auto()
plusget_own_mesh_address()/get_device_info()to include the
connected device itself in the result (the peer-discovery calls only
ever report OTHER devices, never themselves) -- so a single-device
tank correctly comes back with that one device as its sole peer, not
an empty list that would be indistinguishable from "discovery failed
entirely."tank.prefixisNone(andpeersalways empty) if the
connected device didn't report a valid mesh address at all. 3 new
tests. Also exportedmesh_local_prefix_from_mlprefix()from the
top-levelmobiuspackage -- a real gap from when it was first added,
caught while adding these new exports alongside it. -
mobius-scan's default behavior changed: now genuinely just scans
and lists devices grouped by tank, no connections at all -- it used
to print that same pan_id-grouped listing (already existed, already
connection-free) and then STILL go on to connect to every single
scanned device for a full summary regardless. That's now opt-in via a
new--full-summariesflag instead of being the implicit default.
Added--dump-tanks: connects to just ONE device per pan_id group
(not every device) and calls the newdiscover_tank()on it, printing
the full mesh-address/age for every device sharing that tank --
richer than the default listing, which only has whatever's in the BLE
advertisement (model/serial), not address or age.--scan-onlykept
accepted as a no-op (it's redundant now that its old behavior IS the
default) rather than removed, so no existing script using it
explicitly breaks. Renamed the printed header from "Pan ID groups" to
"Tanks" to match. Verified end-to-end with the scan/connect layer
mocked out, for both the new default (confirmed zero MobiusDevice
connections created) and--dump-tanks(confirmed it correctly
prints prefix/address/age for a 2-device tank), not just via
--help's rendered text. -
discover_networked_thread_devices()confirmed against real
hardware (a live 4-device tank: 2 VorTech pumps, 2 Radion lights) --
the byte layout, little-endian field order, and all 4 real peers'
model/serial/age/derived-address all matched exactly. One real
correction from this: empty slots on real hardware are encoded as 28
bytes of raw zero (an all-null-byte serial), not the ASCII
"00000000000000"sentinel stringdiscover_mesh_peers()'s older
mechanism uses -- the existingif not serialfilter already handled
this correctly (an all-null-byte serial decodes to an empty string
after stripping), but the docstring incorrectly claimed this newer
mechanism used the same ASCII convention as the older one. Docstring
corrected; added a dedicated test for the actual null-byte case, plus
a new test replaying the exact real capture end-to-end (all 4 peers,
interleaved with the 26 real empty slots). -
Added
NetworkedThreadDevices(1001) +MLPrefix(2128) support: a
second, better Thread mesh peer-discovery mechanism. Confirmed via
reverse engineering the app's own tank-setup logic to be checked via a
generic support check on the connected device, not gated to a
Cowboy hub the way the existing three-parallel-arrays mechanism
(discover_mesh_peers()) is -- found while investigating whether the
app caches device mesh addresses at all (it does, persisting them to
a local file and syncing them to a cloud backend as part of each
device's saved state; this attribute is what actually populates that
cache in the first place).MobiusDevice.discover_networked_thread_devices()
fetches both attributes and returns every peer the connected device
knows about in a single round-trip, including a per-peer "age" value
(present in the confirmed wire format, but its exact meaning isn't
independently confirmed against real hardware -- seeMeshPeer's own
docstring).MeshPeergained anage: Optional[int] = Nonefield
(only ever set by this new method;Nonefrom every other source, a
non-breaking addition verified against all 4 existingMeshPeer(...)
construction sites, all of which use keyword arguments). Added
mesh_local_prefix_from_mlprefix()tomobius.mesh_address
(MLPrefix's own response is 24 bytes; only the first 8 are the real
prefix, confirmed via reverse engineering). -
Added
MobiusDevice.discover_mesh_peers_auto(): the new
recommended default entry point for mesh peer discovery, replacing
the previous recommendation to calldiscover_mesh_peers()or
discover_mesh_peers_via_direct_connect()directly depending on
hardware. Triesdiscover_networked_thread_devices()first, falling
back todiscover_mesh_peers()only if that finds nothing --
discover_mesh_peers_via_direct_connect()isn't included in this
chain, since it needs a list of scanned devices as input rather than
an already-connectedMobiusDevice; it remains the right fallback to
reach for explicitly if both of the above come back empty. -
On "discovering Tanks": investigated and confirmed not locally
possible -- the app's own "Tank" grouping is account-level and
cloud-synced, with user-edited metadata (name, photo) that has no
local/BLE equivalent to read. The closest local approximation this
library can offer: every peerdiscover_networked_thread_devices()
returns shares the connected device's ownMLPrefix, which in
practice is "every device on the same Thread mesh" -- for a typical
setup, the same thing as "every device in the same aquarium."
Documented as an approximation, not presented as equivalent to the
app's own Tank object. -
mobius-scanCLI updated to match:--dump-mesh-peersand
--relay-targetnow run the newdiscover_mesh_peers_auto()
priority order (step 0:MLPrefix/NetworkedThreadDevices, falling
through to the existing Cowboy-hub-specific steps only if that finds
nothing) instead of only ever exercising the older mechanism;
--dump-mesh-peers' output now also prints each peer'sagewhen
set.--build-peer-mapunchanged in behavior, now documented as the
last-resort fallback when both attribute-based mechanisms come back
empty. 10 new tests covering the new discovery method, the prefix
helper, and both branches ofdiscover_mesh_peers_auto()'s fallback
logic -- plus two direct end-to-end simulations of the updated CLI
debug path (both the new-mechanism-succeeds case and the
falls-through-to-the-old-mechanism case) run manually before
delivery, not just unit-level mocks.
Downloads
-
Source code (ZIP)
2 downloads
-
Source code (TAR.GZ)
1 download
-
-
released this
2026-08-05 14:48:08 +01:00 | 35 commits to main since this release- Fixed a real, high-impact bug:
Brightness(the master dimmer
channel) was never being applied to any other channel at all.
Confirmed directly from raw smali (LightPrimitive.flatten(), called
on both bracketing schedule points ingetIntensitiesAtTime()before
interpolating between them -- JADX's decompiled Java for this class
carried its own "Code duplicated" warning, the same signal that led to
finding theis_night_segment()bug, so this was re-verified against
bytecode rather than trusted from the decompile a second time). Every
channel -- moonlight included -- was being interpolated and returned
without ever being scaled byBrightness, found via real hardware
testing: a light showing ~1% in the app's own display came back as
~25% from this library at the same moment, withBrightnessitself
reading ~3.3% at that point in its own ramp -- fully consistent with
the missing multiplication, not a lunar or scheduling bug.
interpolate_light_schedule()now flattens each bracketing point
SEPARATELY (by that point's ownBrightnessvalue) BEFORE
interpolating between the two already-flattened points -- confirmed
via a dedicated test that this is NOT mathematically equivalent to
interpolating raw values first and multiplying by a single
interpolatedBrightnessat the end, wheneverBrightnessitself
changes across the segment (which it usually does, e.g. ramping
during dusk/dawn).Brightnessitself is still returned as the plain
raw interpolated value, not flattened to a constant 100% the way the
realflatten()does to the primitive it's applied to -- callers
(e.g. a dedicatedBrightnesssensor) want the actual current
master-dimmer level. 4 new tests.
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
0 downloads
- Fixed a real, high-impact bug: