-
v0.2.0-rc.5
Pre-releaseSome checks failedCI / Shell scripts and documentation links (push) Successful in 15sCI / Lint, type-check, test (library, 3.11.0) (push) Successful in 33sCI / Lint, type-check, test (library, 3.14) (push) Successful in 32sCI / Test the dependency floors the package advertises (push) Successful in 27sPublish / Publish to PyPI (push) Successful in 26sCI / Type-check + test the integration (push) Successful in 2m36sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelledPublish / Update the Homebrew tap formula (stable or -rc per tag) (push) Has been cancelledPublish / Bridge the macOS .pkg → NAS (push) Has been cancelledreleased this
2026-08-10 21:56:42 -07:00 | 144 commits to main since this releaseThanks to @CryingPecan, whose LitterHopper robot
on ESP 1.4.4 is behind much of what's below.Protocol detail for all of the below lives in
docs/devices/litter-robot-4/.Added
- The clean cycle is back, and reset with it. Both work by synthesising a
panel button press — the code the robot already emits when you press it —
proven on ESP 1.1.75 and independently confirmed on 1.4.4. This reverses the
0.1.2 removal, which pulled a clean-cycle button built on a byte that never was
the cycle. - Pet weight actually works, and reports the whole cat. It is carried only in
the activity stream, which was previously used as a poll trigger and thrown
away — and the raw-to-pounds divisor was inherited, never checked. A weighed
comparison showed it reporting half: a cat measuring ~8.1 lb came back as 4.08. - New entities: last cat visit, last visit duration, waste drawer last moved,
and panel brightness for bright and dark rooms. - Empty cycle and Power buttons, both disabled by default and named
(danger): an empty cycle dumps the whole globe into the drawer, and Power
toggles the robot off the network. The CLI gainsempty-cycleandpower,
which prompt before sending. - LitterHopper support: connected, fill gauge, and an out-of-litter alert the
firmware itself never raises. The four hopper entities turn themselves on the
first time a hopper reports; one you disable by hand stays off. - Litter percentage calibration. One button press with the globe filled to
the line anchors the percentage to your robot; a second with it empty upgrades
to a true two-point scale. - Activity-derived entities survive a restart instead of reading unknown
until the next cat visit or dispense. - The documentation gained the sections the quality scale asks for and the
self-assessment was claiming without them: supported devices, how data is
updated, what people use it for, and how to remove the integration. hassfest
skipsquality_scale.yamlfor custom integrations, so CI now measures
integration coverage itself and gates the config flow at the 100% the scale
requires.
Fixed
- The filter-change wizard is decoded — its status, both cycle phases, and
the fact that litter readings during it are meaningless. It cannot be started
remotely: the panel chord is a long press, and the robot performs short presses
over MQTT while declining long ones. - Every
0x0Bannunciator value is named (bonnet, cat, cycle, ready, night
light, reset), which closes the "random housekeeping chatter" question. robotStatus10 is the clean cycle, on every firmware. The old map called
it a cat pause; a narrated live cycle disproved that. Also adds5,6/7,
25, the power-up states1/2/3, the boot cycle13, and the filter
wizard14. The last two matter: both move or invert the globe, and while
unmapped their ToF readings were published as real litter levels.- The empty-litter calibration button is available without hunting for it. It
shipped disabled, including for anyone who already had it registered. - The weekday sleep schedule now arms every day. That setting is a per-day
bitmask, not a switch, and turning it on wrote the value for Sunday alone — so
it appeared to work if you tested on a Sunday and did nothing all week. - The cycle phase ladder now runs
2 → 3 → 4 → 5 → 1, so robots no longer
publishunknown_4mid-cycle. - Litter readings are suppressed while the globe is not level — mid-cycle the
sensors read the globe, not the litter, and it was published as a real level. - Broker failures are reported, not raised as a traceback.
- The CLI works against a CA generated by this project's own documentation,
which Python 3.13+ rejected under strict X.509 verification. - The panel sleep schedule can now actually be set. The sleep and wake time
entities wrote a register the firmware only computes, so they never did
anything; they now write the real per-weekday schedule. - Panel sleep mode says what is actually wrong instead of timing out. The
robot derives it from the weekday sleep schedule, which is the switch to use. - The waste-drawer sensor no longer guesses which way the drawer went.
Breaking:binary_sensor.<robot>_waste_drawer_removedis replaced by
sensor.<robot>_waste_drawer_last_moved. Nine different codes turned up across
removals and insertions alike, and a direct read answers the same value either
way, so the robot reports that the drawer moved and nothing more. - One dispense can no longer prove an empty hopper on its own. The floor is
meant to need corroboration across separate dispenses, but detecting a hopper
for the first time reloads the entry, and the fresh coordinator started with an
open deduplication window — so a redelivery of that same first dispense, which
is exactly when one is most likely to still be in flight, counted as a second
one. Only ever affected the first dispense of a robot's life, and only robots
that have a hopper at all. - The hopper stops dropping to unknown. A link code we cannot name is no
longer treated as a disconnect — one such code repeats on a healthy, dispensing
hopper. Only a proven disconnect changes the state. - A multi-register write no longer loses one of its parts. Two commands sent
back to back were seen landing as one; they are now paced and each is verified. - Pet weight can no longer report a cat 100× too heavy. It fell back to a
state-document field in unknown units that no captured robot actually sends. - Panel brightness is verified rather than assumed. The library reported every
write as successful without checking; it now reads the value back like the rest. pic_firmware, previously always null, composed from the localmb*fields.- Entities a previous version created but this one no longer produces are removed
from the registry instead of lingering forever as unavailable. - The example dashboard card no longer lists
select.<robot>_clean_cycle_wait_time,
an entity that stopped existing when that control became a number. whiskerless set night-light-mode autoworks. The name-to-number map was
passed asdict.get's default argument, which Python evaluates eagerly, so
every spelling the map existed to accept —off,on,auto— raised before
the lookup could return. It crashed with a traceback rather than an error, and
it is the exact command in the README quickstart.- The library has tests where it had none. The CLI was at 0% despite shipping
on PyPI, andlink.apply_setting— the command line's own copy of the
write-verify-retry loop — was barely exercised. Adds the destructive-action
confirmation prompts (including thatpowerhas no--yesto skip it), the
rawsendguard path, both transports' retry and give-up behaviour, and the
safety chokepoint's last two branches, plus BLE provisioning driven end to end
against a faked radio — the one irreversible thing here, and previously the
least tested. Library coverage 55% → 99%, withsafety.pyat 100% and both
numbers now gated in CI. - The invariants that only ran when someone remembered are now tests. The
version stamper that keeps four release strings in agreement had none, and a
disagreement there breaks a HACS install with no other symptom. Joined by
checks thatstrings.jsonand the shipped English still match, that every
entity resolves to a translated name, thatquality_scale.yamlstill covers
the real rule list, and that the example automations reference entities that
exist — each of which caught a real defect by hand first. - The declared Home Assistant minimum was wrong.
hacs.jsonsaid 2025.2.0,
but every platform importsAddConfigEntryEntitiesCallback, which Home
Assistant only gained in 2025.3.0. HACS enforces that minimum, so a user on
2025.2 could install this and watch it fail at import. Nothing caught it
because the integration tests only ever run against the newest Home Assistant. - The write paths are tested. Integration coverage went from 93% to 99% —
it had been under the 95% the quality scale asks for while claiming to meet it.
Reads were well covered and writes barely were, which is backwards: a decode
bug shows a wrong number, a write bug changes the robot. Adds per-platform
write tests, every panel press, the activity-stream entities, a snapshot of the
whole entity surface, and the coordinator's retry, timeout, unload and
broker-not-ready paths. CI now gates the numbers instead of reporting them. - The Refresh button ships enabled. It is a read-only state request, and it is
what the troubleshooting docs tell you to press when a robot has gone quiet —
being sent to enable an entity first was friction at the worst moment. Existing
installs get it turned on too, unless you disabled it by hand.
Changed
-
Both test suites are gated at 99% coverage, up from 98% (library) and 95%
(integration). The quality scale only asks for 95%; the gates now match what the
suites actually hold, so a regression fails the build instead of quietly eating
the headroom.safety.pyand the config flow stay at their separate 100%. -
Last visit duration enables itself, like the hopper entities. It needs ESP
1.4.4: a 12-hour capture of a 1.1.75 robot logged five cat visits and three pet
weights without one duration, and on 1.4.4 a weight always comes with a
duration, so the register is absent rather than the visits being short. It now
ships disabled and switches on the first time a robot reports one, carrying that
first reading — rather than being a sensor that reads unknown for the life of a
1.1.75 robot. -
Cat detected is documented as following the scale, not the sensors looking
into the globe, so it can stay on for hours after a cat leaves; the same capture
spent 29% of its time reporting a cat over an undisturbed litter bed. The entity
list also gained the four entities that ship but were never listed. -
The Linux binaries would not have started on Debian 12, RHEL 9 or an older
Pi OS. PyInstaller freezes the interpreter but links against the build
machine's glibc, so building on a current Ubuntu quietly required glibc 2.39
— and nothing noticed, because the binary runs fine on the runner that made
it. They are now built in a manylinux_2_28 container and a release fails if
any bundled ELF asks for more than the declared floor. -
The packages are installed before they are published. The
.debgoes on
Debian 12 and the.rpmon Rocky 9, and the binary is run, before a release
can carry them — both distros sit above the declared glibc floor and below
the build runner's, which is the window where this class of bug lives. CI
also shellchecks the release scripts and fails on a broken documentation
link, neither of which anything checked before. -
Linux gets arm64 binaries and real packages.
.deband.rpmfor both
architectures, plus an arm64 build of the standalone provisioner for
Raspberry Pis and arm servers — the x86_64 binary already shipped. Neither
package needs a system Python; PyInstaller bundles it, which is the point on
a laptop that has none. -
brew install sisyphusmd/tap/whiskerless, with a separate
whiskerless-rcchannel for candidates. A source install into a virtualenv,
so it needs no notarization and covers macOS and Linux on both architectures
from one file. The formula checksum is taken from an sdist built locally at
the tag and then required to match what PyPI serves — a registry download is
what that checksum exists to protect against, so it is never its source. -
CI tests what the package advertises, not just what resolves today. The
library suite now runs on macOS as well as Linux (the provisioner binary
ships for macOS, and bleak's backend differs by platform), and a new job
installs the oldest aiomqtt and bleak that pyproject claims to support.
The Python floor leg now pins 3.11.0 rather than "3.11", which resolved to
the newest patch and so tested a floor nobody advertises. macOS is tested at
its oldest and current releases on both architectures. The prerelease gate
re-checks the floors in their own job, becauseuvleaves a requirement the
environment already satisfies alone and would otherwise test latest twice. -
Release publishing survives its own concurrency. Three workflows race to
create each release; the helpers did a check-then-create, so a loser hit
"already exists" and failed the job. They now adopt the winner's release,
whose notes are the same CHANGELOG section they were about to write. -
The clean cycle, reset and empty presses no longer need a motor opt-in.
Breaking (library):Hazard.MOTOR,MotorCommandErrorand the
allow_motorargument are gone. Writing the panel button register reproduces
the code the panel emits, so a written press is the same event as a physical
one and the robot's own interlocks apply either way. Power still requires
allow_dangerous. -
Clean cycle wait time is now a number (3–30 minutes), not a select.
Breaking: automations usingselect.<robot>_clean_cycle_wait_timemust
move tonumber.<robot>_clean_cycle_wait_time. -
Panel brightness High/Low name the ambient light level, not the brightness
rank. The stock 40/50 is deliberately brighter at night. -
isPanelSleepModeand the panel sleep/wake times are documented as read-only:
the firmware computes all three from the weekday schedule registers. -
The register map now says what PROVEN has to mean, demotes the rows that never
earned it, and documents the readable file as0x00-0x7Fwith 123 of those 128
addresses mapped.unitPowerTypeis pinned (0 = mains, 1 = battery) and
isUSBPowerOnturns out to mean mains present, not USB.0x02A10000returns
Wi-Fi RSSI only, not the schedule. -
Releases are never published before the library version they pin exists on
PyPI, and CI now exercises the Python 3.11 floor it advertises.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The clean cycle is back, and reset with it. Both work by synthesising a
-
v0.2.0-rc.4
Pre-releaseSome checks failedCI / Lint, type-check, test (library, 3.11.0) (push) Successful in 33sCI / Lint, type-check, test (library, 3.14) (push) Successful in 31sCI / Shell scripts and documentation links (push) Successful in 19sCI / Test the dependency floors the package advertises (push) Successful in 29sPublish / Publish to PyPI (push) Successful in 30sCI / Type-check + test the integration (push) Successful in 2m52sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelledPublish / Update the Homebrew tap formula (stable or -rc per tag) (push) Has been cancelledPublish / Bridge the macOS .pkg → NAS (push) Has been cancelledreleased this
2026-08-10 21:16:50 -07:00 | 145 commits to main since this releaseThanks to @CryingPecan, whose LitterHopper robot
on ESP 1.4.4 is behind much of what's below.Protocol detail for all of the below lives in
docs/devices/litter-robot-4/.Added
- The clean cycle is back, and reset with it. Both work by synthesising a
panel button press — the code the robot already emits when you press it —
proven on ESP 1.1.75 and independently confirmed on 1.4.4. This reverses the
0.1.2 removal, which pulled a clean-cycle button built on a byte that never was
the cycle. - Pet weight actually works, and reports the whole cat. It is carried only in
the activity stream, which was previously used as a poll trigger and thrown
away — and the raw-to-pounds divisor was inherited, never checked. A weighed
comparison showed it reporting half: a cat measuring ~8.1 lb came back as 4.08. - New entities: last cat visit, last visit duration, waste drawer last moved,
and panel brightness for bright and dark rooms. - Empty cycle and Power buttons, both disabled by default and named
(danger): an empty cycle dumps the whole globe into the drawer, and Power
toggles the robot off the network. The CLI gainsempty-cycleandpower,
which prompt before sending. - LitterHopper support: connected, fill gauge, and an out-of-litter alert the
firmware itself never raises. The four hopper entities turn themselves on the
first time a hopper reports; one you disable by hand stays off. - Litter percentage calibration. One button press with the globe filled to
the line anchors the percentage to your robot; a second with it empty upgrades
to a true two-point scale. - Activity-derived entities survive a restart instead of reading unknown
until the next cat visit or dispense. - The documentation gained the sections the quality scale asks for and the
self-assessment was claiming without them: supported devices, how data is
updated, what people use it for, and how to remove the integration. hassfest
skipsquality_scale.yamlfor custom integrations, so CI now measures
integration coverage itself and gates the config flow at the 100% the scale
requires.
Fixed
- The filter-change wizard is decoded — its status, both cycle phases, and
the fact that litter readings during it are meaningless. It cannot be started
remotely: the panel chord is a long press, and the robot performs short presses
over MQTT while declining long ones. - Every
0x0Bannunciator value is named (bonnet, cat, cycle, ready, night
light, reset), which closes the "random housekeeping chatter" question. robotStatus10 is the clean cycle, on every firmware. The old map called
it a cat pause; a narrated live cycle disproved that. Also adds5,6/7,
25, the power-up states1/2/3, the boot cycle13, and the filter
wizard14. The last two matter: both move or invert the globe, and while
unmapped their ToF readings were published as real litter levels.- The empty-litter calibration button is available without hunting for it. It
shipped disabled, including for anyone who already had it registered. - The weekday sleep schedule now arms every day. That setting is a per-day
bitmask, not a switch, and turning it on wrote the value for Sunday alone — so
it appeared to work if you tested on a Sunday and did nothing all week. - The cycle phase ladder now runs
2 → 3 → 4 → 5 → 1, so robots no longer
publishunknown_4mid-cycle. - Litter readings are suppressed while the globe is not level — mid-cycle the
sensors read the globe, not the litter, and it was published as a real level. - Broker failures are reported, not raised as a traceback.
- The CLI works against a CA generated by this project's own documentation,
which Python 3.13+ rejected under strict X.509 verification. - The panel sleep schedule can now actually be set. The sleep and wake time
entities wrote a register the firmware only computes, so they never did
anything; they now write the real per-weekday schedule. - Panel sleep mode says what is actually wrong instead of timing out. The
robot derives it from the weekday sleep schedule, which is the switch to use. - The waste-drawer sensor no longer guesses which way the drawer went.
Breaking:binary_sensor.<robot>_waste_drawer_removedis replaced by
sensor.<robot>_waste_drawer_last_moved. Nine different codes turned up across
removals and insertions alike, and a direct read answers the same value either
way, so the robot reports that the drawer moved and nothing more. - One dispense can no longer prove an empty hopper on its own. The floor is
meant to need corroboration across separate dispenses, but detecting a hopper
for the first time reloads the entry, and the fresh coordinator started with an
open deduplication window — so a redelivery of that same first dispense, which
is exactly when one is most likely to still be in flight, counted as a second
one. Only ever affected the first dispense of a robot's life, and only robots
that have a hopper at all. - The hopper stops dropping to unknown. A link code we cannot name is no
longer treated as a disconnect — one such code repeats on a healthy, dispensing
hopper. Only a proven disconnect changes the state. - A multi-register write no longer loses one of its parts. Two commands sent
back to back were seen landing as one; they are now paced and each is verified. - Pet weight can no longer report a cat 100× too heavy. It fell back to a
state-document field in unknown units that no captured robot actually sends. - Panel brightness is verified rather than assumed. The library reported every
write as successful without checking; it now reads the value back like the rest. pic_firmware, previously always null, composed from the localmb*fields.- Entities a previous version created but this one no longer produces are removed
from the registry instead of lingering forever as unavailable. - The example dashboard card no longer lists
select.<robot>_clean_cycle_wait_time,
an entity that stopped existing when that control became a number. whiskerless set night-light-mode autoworks. The name-to-number map was
passed asdict.get's default argument, which Python evaluates eagerly, so
every spelling the map existed to accept —off,on,auto— raised before
the lookup could return. It crashed with a traceback rather than an error, and
it is the exact command in the README quickstart.- The library has tests where it had none. The CLI was at 0% despite shipping
on PyPI, andlink.apply_setting— the command line's own copy of the
write-verify-retry loop — was barely exercised. Adds the destructive-action
confirmation prompts (including thatpowerhas no--yesto skip it), the
rawsendguard path, both transports' retry and give-up behaviour, and the
safety chokepoint's last two branches, plus BLE provisioning driven end to end
against a faked radio — the one irreversible thing here, and previously the
least tested. Library coverage 55% → 99%, withsafety.pyat 100% and both
numbers now gated in CI. - The invariants that only ran when someone remembered are now tests. The
version stamper that keeps four release strings in agreement had none, and a
disagreement there breaks a HACS install with no other symptom. Joined by
checks thatstrings.jsonand the shipped English still match, that every
entity resolves to a translated name, thatquality_scale.yamlstill covers
the real rule list, and that the example automations reference entities that
exist — each of which caught a real defect by hand first. - The declared Home Assistant minimum was wrong.
hacs.jsonsaid 2025.2.0,
but every platform importsAddConfigEntryEntitiesCallback, which Home
Assistant only gained in 2025.3.0. HACS enforces that minimum, so a user on
2025.2 could install this and watch it fail at import. Nothing caught it
because the integration tests only ever run against the newest Home Assistant. - The write paths are tested. Integration coverage went from 93% to 99% —
it had been under the 95% the quality scale asks for while claiming to meet it.
Reads were well covered and writes barely were, which is backwards: a decode
bug shows a wrong number, a write bug changes the robot. Adds per-platform
write tests, every panel press, the activity-stream entities, a snapshot of the
whole entity surface, and the coordinator's retry, timeout, unload and
broker-not-ready paths. CI now gates the numbers instead of reporting them. - The Refresh button ships enabled. It is a read-only state request, and it is
what the troubleshooting docs tell you to press when a robot has gone quiet —
being sent to enable an entity first was friction at the worst moment. Existing
installs get it turned on too, unless you disabled it by hand.
Changed
-
Both test suites are gated at 99% coverage, up from 98% (library) and 95%
(integration). The quality scale only asks for 95%; the gates now match what the
suites actually hold, so a regression fails the build instead of quietly eating
the headroom.safety.pyand the config flow stay at their separate 100%. -
Last visit duration enables itself, like the hopper entities. It needs ESP
1.4.4: a 12-hour capture of a 1.1.75 robot logged five cat visits and three pet
weights without one duration, and on 1.4.4 a weight always comes with a
duration, so the register is absent rather than the visits being short. It now
ships disabled and switches on the first time a robot reports one, carrying that
first reading — rather than being a sensor that reads unknown for the life of a
1.1.75 robot. -
Cat detected is documented as following the scale, not the sensors looking
into the globe, so it can stay on for hours after a cat leaves; the same capture
spent 29% of its time reporting a cat over an undisturbed litter bed. The entity
list also gained the four entities that ship but were never listed. -
The Linux binaries would not have started on Debian 12, RHEL 9 or an older
Pi OS. PyInstaller freezes the interpreter but links against the build
machine's glibc, so building on a current Ubuntu quietly required glibc 2.39
— and nothing noticed, because the binary runs fine on the runner that made
it. They are now built in a manylinux_2_28 container and a release fails if
any bundled ELF asks for more than the declared floor. -
The packages are installed before they are published. The
.debgoes on
Debian 12 and the.rpmon Rocky 9, and the binary is run, before a release
can carry them — both distros sit above the declared glibc floor and below
the build runner's, which is the window where this class of bug lives. CI
also shellchecks the release scripts and fails on a broken documentation
link, neither of which anything checked before. -
Linux gets arm64 binaries and real packages.
.deband.rpmfor both
architectures, plus an arm64 build of the standalone provisioner for
Raspberry Pis and arm servers — the x86_64 binary already shipped. Neither
package needs a system Python; PyInstaller bundles it, which is the point on
a laptop that has none. -
brew install sisyphusmd/tap/whiskerless, with a separate
whiskerless-rcchannel for candidates. A source install into a virtualenv,
so it needs no notarization and covers macOS and Linux on both architectures
from one file. The formula checksum is taken from an sdist built locally at
the tag and then required to match what PyPI serves — a registry download is
what that checksum exists to protect against, so it is never its source. -
CI tests what the package advertises, not just what resolves today. The
library suite now runs on macOS as well as Linux (the provisioner binary
ships for macOS, and bleak's backend differs by platform), and a new job
installs the oldest aiomqtt and bleak that pyproject claims to support.
The Python floor leg now pins 3.11.0 rather than "3.11", which resolved to
the newest patch and so tested a floor nobody advertises. macOS is tested at
its oldest and current releases on both architectures. The prerelease gate
re-checks the floors in their own job, becauseuvleaves a requirement the
environment already satisfies alone and would otherwise test latest twice. -
Release publishing survives its own concurrency. Three workflows race to
create each release; the helpers did a check-then-create, so a loser hit
"already exists" and failed the job. They now adopt the winner's release,
whose notes are the same CHANGELOG section they were about to write. -
The clean cycle, reset and empty presses no longer need a motor opt-in.
Breaking (library):Hazard.MOTOR,MotorCommandErrorand the
allow_motorargument are gone. Writing the panel button register reproduces
the code the panel emits, so a written press is the same event as a physical
one and the robot's own interlocks apply either way. Power still requires
allow_dangerous. -
Clean cycle wait time is now a number (3–30 minutes), not a select.
Breaking: automations usingselect.<robot>_clean_cycle_wait_timemust
move tonumber.<robot>_clean_cycle_wait_time. -
Panel brightness High/Low name the ambient light level, not the brightness
rank. The stock 40/50 is deliberately brighter at night. -
isPanelSleepModeand the panel sleep/wake times are documented as read-only:
the firmware computes all three from the weekday schedule registers. -
The register map now says what PROVEN has to mean, demotes the rows that never
earned it, and documents the readable file as0x00-0x7Fwith 123 of those 128
addresses mapped.unitPowerTypeis pinned (0 = mains, 1 = battery) and
isUSBPowerOnturns out to mean mains present, not USB.0x02A10000returns
Wi-Fi RSSI only, not the schedule. -
Releases are never published before the library version they pin exists on
PyPI, and CI now exercises the Python 3.11 floor it advertises.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The clean cycle is back, and reset with it. Both work by synthesising a
-
v0.2.0-rc.3
Pre-releaseSome checks failedCI / Lint, type-check, test (library, 3.11) (push) Successful in 27sCI / Lint, type-check, test (library, 3.14) (push) Successful in 33sPublish / Publish to PyPI (push) Successful in 29sCI / Type-check + test the integration (push) Successful in 2m12sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelledPublish / Bridge the macOS .pkg → NAS (push) Has been cancelledreleased this
2026-08-09 23:23:19 -07:00 | 170 commits to main since this releaseMost of this release came from a field install by
@CryingPecan, who ran a Litter-Robot 4 with a
LitterHopper on ESP 1.4.4 for two weeks of MQTT capture. Protocol detail for all
of the below lives indocs/devices/litter-robot-4/.Added
- The clean cycle is back, and reset with it. Both work by synthesising a
panel button press — the code the robot already emits when you press it —
proven on ESP 1.1.75 and independently confirmed on 1.4.4. This reverses the
0.1.2 removal, which pulled a clean-cycle button built on a byte that never was
the cycle. - Pet weight actually works, and reports the whole cat. It is carried only in
the activity stream, which was previously used as a poll trigger and thrown
away — and the raw-to-pounds divisor was inherited, never checked. A weighed
comparison showed it reporting half: a cat measuring ~8.1 lb came back as 4.08. - New entities: last cat visit, last visit duration, waste drawer last moved,
and panel brightness for bright and dark rooms. - Empty cycle and Power buttons, both disabled by default and named
(danger): an empty cycle dumps the whole globe into the drawer, and Power
toggles the robot off the network. The CLI gainsempty-cycleandpower,
which prompt before sending. - LitterHopper support: connected, fill gauge, and an out-of-litter alert the
firmware itself never raises. The four hopper entities turn themselves on the
first time a hopper reports; one you disable by hand stays off. - Litter percentage calibration. One button press with the globe filled to
the line anchors the percentage to your robot; a second with it empty upgrades
to a true two-point scale. - Activity-derived entities survive a restart instead of reading unknown
until the next cat visit or dispense.
Fixed
- The filter-change wizard is decoded — its status, both cycle phases, and
the fact that litter readings during it are meaningless. It cannot be started
remotely: the panel chord is a long press, and the robot performs short presses
over MQTT while declining long ones. - Every
0x0Bannunciator value is named (bonnet, cat, cycle, ready, night
light, reset), which closes the "random housekeeping chatter" question. robotStatus10 is the clean cycle, on every firmware. The old map called
it a cat pause; a narrated live cycle disproved that. Also adds5,6/7,
25, the power-up states1/2/3, the boot cycle13, and the filter
wizard14. The last two matter: both move or invert the globe, and while
unmapped their ToF readings were published as real litter levels.- The empty-litter calibration button is available without hunting for it. It
shipped disabled, including for anyone who already had it registered. - The weekday sleep schedule now arms every day. That setting is a per-day
bitmask, not a switch, and turning it on wrote the value for Sunday alone — so
it appeared to work if you tested on a Sunday and did nothing all week. - The cycle phase ladder now runs
2 → 3 → 4 → 5 → 1, so robots no longer
publishunknown_4mid-cycle. - Litter readings are suppressed while the globe is not level — mid-cycle the
sensors read the globe, not the litter, and it was published as a real level. - Broker failures are reported, not raised as a traceback.
- The CLI works against a CA generated by this project's own documentation,
which Python 3.13+ rejected under strict X.509 verification. - The panel sleep schedule can now actually be set. The sleep and wake time
entities wrote a register the firmware only computes, so they never did
anything; they now write the real per-weekday schedule. - Panel sleep mode says what is actually wrong instead of timing out. The
robot derives it from the weekday sleep schedule, which is the switch to use. - The waste-drawer sensor no longer guesses which way the drawer went.
Breaking:binary_sensor.<robot>_waste_drawer_removedis replaced by
sensor.<robot>_waste_drawer_last_moved. Nine different codes turned up across
removals and insertions alike, and a direct read answers the same value either
way, so the robot reports that the drawer moved and nothing more. - The hopper stops dropping to unknown. A link code we cannot name is no
longer treated as a disconnect — one such code repeats on a healthy, dispensing
hopper. Only a proven disconnect changes the state. - A multi-register write no longer loses one of its parts. Two commands sent
back to back were seen landing as one; they are now paced and each is verified. - Pet weight can no longer report a cat 100× too heavy. It fell back to a
state-document field in unknown units that no captured robot actually sends. - Panel brightness is verified rather than assumed. The library reported every
write as successful without checking; it now reads the value back like the rest. pic_firmware, previously always null, composed from the localmb*fields.- Entities a previous version created but this one no longer produces are removed
from the registry instead of lingering forever as unavailable.
Changed
- The clean cycle, reset and empty presses no longer need a motor opt-in.
Breaking (library):Hazard.MOTOR,MotorCommandErrorand the
allow_motorargument are gone. Writing the panel button register reproduces
the code the panel emits, so a written press is the same event as a physical
one and the robot's own interlocks apply either way. Power still requires
allow_dangerous. - Clean cycle wait time is now a number (3–30 minutes), not a select.
Breaking: automations usingselect.<robot>_clean_cycle_wait_timemust
move tonumber.<robot>_clean_cycle_wait_time. - Panel brightness High/Low name the ambient light level, not the brightness
rank. The stock 40/50 is deliberately brighter at night. isPanelSleepModeand the panel sleep/wake times are documented as read-only:
the firmware computes all three from the weekday schedule registers.- The register map now says what PROVEN has to mean, demotes the rows that never
earned it, and documents the readable file as0x00-0x7Fwith 123 of those 128
addresses mapped.unitPowerTypeis pinned (0 = mains, 1 = battery) and
isUSBPowerOnturns out to mean mains present, not USB.0x02A10000returns
Wi-Fi RSSI only, not the schedule. - Releases are never published before the library version they pin exists on
PyPI, and CI now exercises the Python 3.11 floor it advertises.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The clean cycle is back, and reset with it. Both work by synthesising a
-
v0.2.0-rc.2
Pre-releaseSome checks failedCI / Lint, type-check, test (library, 3.11) (push) Successful in 27sCI / Lint, type-check, test (library, 3.14) (push) Successful in 29sPublish / Publish to PyPI (push) Successful in 36sCI / Type-check + test the integration (push) Successful in 2m5sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelledPublish / Bridge the macOS .pkg → NAS (push) Has been cancelledreleased this
2026-08-09 11:06:49 -07:00 | 186 commits to main since this releaseMost of this release came from a field install by
@CryingPecan, who ran a real Litter-Robot 4
with a LitterHopper on ESP 1.4.4 for two weeks of continuous MQTT capture and
turned it into four PRs and a pile of protocol findings.Added
- Pet weight actually works. Weight (register
0x09) is reported only in the
activity stream, which the coordinator previously used as a poll trigger and
discarded — socat_weightwas permanently null. New libraryevents.py
decodes the activity stream into typed events. - New entities: last cat visit, last visit duration (register
0xBC, which
also gates whether a weight event fires at all), waste drawer removed
(register0x56— something the cloud integration does not offer), and panel
brightness for bright and dark rooms. - LitterHopper support: hopper connected, hopper fill gauge, and a hopper
out-of-litter alert. The firmware never flags an empty hopper — it keeps
running a full dispense every cycle, delivering nothing — so the gauge is read
from dispense phase 1, calibrated across a live drain-to-refill arc. Neither
the cloud nor the Whisker app offers this. These four ship disabled: the
hopper is optional hardware and invisible in the local state document, so
enable them once if your robot has one. - Litter percentage calibration.
litterLevelis a distance, and the cloud
converts it against a per-robot reference that is not in the local state
document. One button press with the globe filled to the line anchors the
percentage to your robot; an optional second press with it empty upgrades to a
true two-point scale. - Activity-derived entities survive a restart instead of reading unknown
until the next cat visit or dispense.
Fixed
robotStatus10 is the clean cycle, on every firmware. This table has said
10= cat/weight pause and13= cycling since the first commit, tagged
PROVEN. It came from a static firmware-RE brief and was never checked against a
live cycle. A narrated manual cycle on ESP 1.1.75 holdsrobotStatusat10
for the entire cycle withcatDetectat0throughout, and13has never
appeared on either captured firmware. Also adds5,6/7and25.- The cycle phase ladder
2 → 3 → 4 → 5 → 1(the cloud names them DUMP, DFI,
LEVEL, HOME, IDLE). The map previously stopped at 3, so every robot published
unknown_4mid-cycle.robotCycleState 4is the cat-interrupt pause, which
self-clears without a button press. - Litter readings are suppressed while the globe is not level — mid-cycle the
sensors read the rotating globe rather than the litter bed (575 mm observed on
a 453 mm fill), which was published as a real percentage. Also covers the
filter-change wizard, which parks the globe inverted. - Broker failures are reported, not raised as a traceback. An unreachable
broker or rejected certificate produced a raw Python stack trace. - The CLI works against a CA generated by this project's own documentation.
Python 3.13 enables strict X.509 verification, which rejects a CA without a
keyUsageextension — and the macOS installer has no older interpreter to fall
back to. Strictness is now relaxed only for an explicitly pinned CA, which is
also now the only certificate trusted on that connection. pic_firmware, previously always null, composed from the localmb*fields.- Entities a previous version created but this one no longer produces are removed
from the registry instead of lingering forever as unavailable.
Changed
- Clean cycle wait time is now a number (3–30 minutes), not a select. The
robot accepts any value in the range; the five presets were inherited from
pylitterbot and made any other setting read asunknown. Breaking:
automations referencingselect.<robot>_clean_cycle_wait_timemust move to
number.<robot>_clean_cycle_wait_time. - Panel brightness High/Low name the ambient light level, not the brightness
rank — proven by a two-observation behavioural test. The stock 40/50 is
deliberately brighter at night. - Releases are never published before the library version they pin exists on
PyPI, and CI now exercises the Python 3.11 floor it advertises.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Pet weight actually works. Weight (register
-
v0.2.0-rc.1
Pre-releaseSome checks failedCI / Lint, type-check, test (library, 3.11) (push) Successful in 29sCI / Lint, type-check, test (library, 3.14) (push) Successful in 28sPublish / Publish to PyPI (push) Successful in 28sCI / Type-check + test the integration (push) Successful in 2m0sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Failing after 35sPublish / Bridge the macOS .pkg → NAS (push) Has been skippedreleased this
2026-08-09 01:29:07 -07:00 | 192 commits to main since this releaseMost of this release came from a field install by
@CryingPecan, who ran a real Litter-Robot 4
with a LitterHopper on ESP 1.4.4 for two weeks of continuous MQTT capture and
turned it into four PRs and a pile of protocol findings.Added
- Pet weight actually works. Weight (register
0x09) is reported only in the
activity stream, which the coordinator previously used as a poll trigger and
discarded — socat_weightwas permanently null. New libraryevents.py
decodes the activity stream into typed events. - New entities: last cat visit, last visit duration (register
0xBC, which
also gates whether a weight event fires at all), waste drawer removed
(register0x56— something the cloud integration does not offer), and panel
brightness for bright and dark rooms. - LitterHopper support: hopper connected, hopper fill gauge, and a hopper
out-of-litter alert. The firmware never flags an empty hopper — it keeps
running a full dispense every cycle, delivering nothing — so the gauge is read
from dispense phase 1, calibrated across a live drain-to-refill arc. Neither
the cloud nor the Whisker app offers this. These four ship disabled: the
hopper is optional hardware and invisible in the local state document, so
enable them once if your robot has one. - Litter percentage calibration.
litterLevelis a distance, and the cloud
converts it against a per-robot reference that is not in the local state
document. One button press with the globe filled to the line anchors the
percentage to your robot; an optional second press with it empty upgrades to a
true two-point scale. - Activity-derived entities survive a restart instead of reading unknown
until the next cat visit or dispense.
Fixed
robotStatus10 is the clean cycle, on every firmware. This table has said
10= cat/weight pause and13= cycling since the first commit, tagged
PROVEN. It came from a static firmware-RE brief and was never checked against a
live cycle. A narrated manual cycle on ESP 1.1.75 holdsrobotStatusat10
for the entire cycle withcatDetectat0throughout, and13has never
appeared on either captured firmware. Also adds5,6/7and25.- The cycle phase ladder
2 → 3 → 4 → 5 → 1(the cloud names them DUMP, DFI,
LEVEL, HOME, IDLE). The map previously stopped at 3, so every robot published
unknown_4mid-cycle.robotCycleState 4is the cat-interrupt pause, which
self-clears without a button press. - Litter readings are suppressed while the globe is not level — mid-cycle the
sensors read the rotating globe rather than the litter bed (575 mm observed on
a 453 mm fill), which was published as a real percentage. Also covers the
filter-change wizard, which parks the globe inverted. - Broker failures are reported, not raised as a traceback. An unreachable
broker or rejected certificate produced a raw Python stack trace. - The CLI works against a CA generated by this project's own documentation.
Python 3.13 enables strict X.509 verification, which rejects a CA without a
keyUsageextension — and the macOS installer has no older interpreter to fall
back to. Strictness is now relaxed only for an explicitly pinned CA, which is
also now the only certificate trusted on that connection. pic_firmware, previously always null, composed from the localmb*fields.- Entities a previous version created but this one no longer produces are removed
from the registry instead of lingering forever as unavailable.
Changed
- Clean cycle wait time is now a number (3–30 minutes), not a select. The
robot accepts any value in the range; the five presets were inherited from
pylitterbot and made any other setting read asunknown. Breaking:
automations referencingselect.<robot>_clean_cycle_wait_timemust move to
number.<robot>_clean_cycle_wait_time. - Panel brightness High/Low name the ambient light level, not the brightness
rank — proven by a two-observation behavioural test. The stock 40/50 is
deliberately brighter at night. - Releases are never published before the library version they pin exists on
PyPI, and CI now exercises the Python 3.11 floor it advertises.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Pet weight actually works. Weight (register
-
v0.1.3
StableAll checks were successfulPublish / Publish to PyPI (push) Successful in 53sCI / Type-check + test the integration (push) Successful in 4m12sCI / Lint, type-check, test (library) (push) Successful in 50sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 1m14sPublish / Bridge the macOS .pkg → NAS (push) Successful in 8sreleased this
2026-07-01 22:03:06 -07:00 | 233 commits to main since this releaseAdded
- BLE provisioning now fails on non-LR4 devices. The serial is validated up
front (must be theLR4…form; normalized to the label's uppercase), and after
connecting the provisioner verifies the device exposes the LR4 provisioning
GATT service before writing anything — closing the--addresspath that
previously accepted an arbitrary BLE address.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- BLE provisioning now fails on non-LR4 devices. The serial is validated up
-
v0.1.2
StableAll checks were successfulPublish / Publish to PyPI (push) Successful in 51sCI / Type-check + test the integration (push) Successful in 4m5sCI / Lint, type-check, test (library) (push) Successful in 47sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 1m33sPublish / Bridge the macOS .pkg → NAS (push) Successful in 8sreleased this
2026-06-29 09:06:17 -07:00 | 236 commits to main since this releaseRemoved
- The clean-cycle command — the Home Assistant button, the CLI
clean-cycle
subcommand, andcommands.clean_cycle(). A live capture proved the inherited
0x02A30000("cleanCycle") opcode resets the robot (odometerPowerCycles
increments, no cycle runs) — it was a reset disguised as a cycle, so it's gone
rather than left shipping a surprise reboot.
Changed
0xA3reclassified to never-send. It's the reset / main-board-OTA
orchestrator, not a motor command; the safety guard now refuses it unconditionally
alongside0xA4/0xAC/0xAD. No motor command is exposed until a real
cleanCycle trigger is recovered — theMOTOR/allow_motorgate stays wired but
empty.- Documented the action-command hunt in
docs/reverse-engineering.md: the
0xA3correction, why the cleanCycle / power / empty / reset dispatch lives in a
bootloader region absent from every public OTA image, the exhaustive (and empty)
search for a complete firmware dump, and the recovery paths (cloud-byte capture,
app decompile, ESP-flash / PIC-ICSP dump).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The clean-cycle command — the Home Assistant button, the CLI
-
v0.1.1
StableAll checks were successfulPublish / Publish to PyPI (push) Successful in 26sCI / Type-check + test the integration (push) Successful in 1m52sCI / Lint, type-check, test (library) (push) Successful in 29sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 42sPublish / Bridge the macOS .pkg → NAS (push) Successful in 5sreleased this
2026-06-28 20:27:30 -07:00 | 239 commits to main since this releaseFixed
- The signed macOS installer (Apple Silicon + Intel) now builds and ships;
0.1.0targeted the since-retiredmacos-13runner image, so that release
shipped without the macOS.pkg.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The signed macOS installer (Apple Silicon + Intel) now builds and ships;
-
v0.1.0
StableAll checks were successfulCI / Lint, type-check, test (library) (push) Successful in 25sPublish / Publish to PyPI (push) Successful in 25sCI / Type-check + test the integration (push) Successful in 1m46sPublish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 41sPublish / Bridge the macOS .pkg → NAS (push) Successful in 30m9sreleased this
2026-06-28 18:49:26 -07:00 | 244 commits to main since this releaseAdded
whiskerlessPython library — fully-local MQTT control + telemetry for the
Whisker Litter-Robot 4:- LR4 wire codec, command catalog, and a typed state model that decodes both raw
firmware integers and cloud-style strings defensively. - A push-first
LitterRobot4Clientwith a self-healing MQTT connection and
write → read-back → retry for the firmware's commit-latency registers. - Device-agnostic BLE (esp-idf protocomm) re-provisioning with a self-contained
pure-Python protobuf codec (noprotocbuild step). - A
safetyguard that refuses brick/reset-class commands (0xAC/0xA4/0xAD)
unconditionally and gates motor / untraced commands.
- LR4 wire codec, command catalog, and a typed state model that decodes both raw
whiskerlessCLI —provision,monitor,state,read,set,
clean-cycle, and a guarded rawsend.- Home Assistant integration (HACS) built to Platinum standards: fully async,
fully typed,local_push, with MQTT discovery (robots appear as Add/Ignore
cards), diagnostics, translations, and per-robot config entries (any number of
robots). - Documentation — protocol reference, register map, command catalog,
compatibility matrix, setup guides, recovery guide, and the reverse-engineering
writeup. - Standalone CLI binaries built on release for users who want the BLE
re-provisioner without installing Python.
Downloads
-
Source code (ZIP)
17 downloads
-
Source code (TAR.GZ)
0 downloads