• v0.2.0-rc.5 f4e3d87cfc

    v0.2.0-rc.5
    Some checks failed
    CI / Shell scripts and documentation links (push) Successful in 15s
    CI / Lint, type-check, test (library, 3.11.0) (push) Successful in 33s
    CI / Lint, type-check, test (library, 3.14) (push) Successful in 32s
    CI / Test the dependency floors the package advertises (push) Successful in 27s
    Publish / Publish to PyPI (push) Successful in 26s
    CI / Type-check + test the integration (push) Successful in 2m36s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelled
    Publish / Update the Homebrew tap formula (stable or -rc per tag) (push) Has been cancelled
    Publish / Bridge the macOS .pkg → NAS (push) Has been cancelled
    Pre-release

    Cody Bryant released this 2026-08-10 21:56:42 -07:00 | 144 commits to main since this release

    Thanks 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 gains empty-cycle and power,
      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
      skips quality_scale.yaml for 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 0x0B annunciator value is named (bonnet, cat, cycle, ready, night
      light, reset), which closes the "random housekeeping chatter" question.
    • robotStatus 10 is the clean cycle, on every firmware. The old map called
      it a cat pause; a narrated live cycle disproved that. Also adds 5, 6/7,
      25, the power-up states 1/2/3, the boot cycle 13, and the filter
      wizard 14. 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
      publish unknown_4 mid-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_removed is 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 local mb* 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 auto works. The name-to-number map was
      passed as dict.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, and link.apply_setting — the command line's own copy of the
      write-verify-retry loop — was barely exercised. Adds the destructive-action
      confirmation prompts (including that power has no --yes to skip it), the
      raw send guard 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%, with safety.py at 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 that strings.json and the shipped English still match, that every
      entity resolves to a translated name, that quality_scale.yaml still 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.json said 2025.2.0,
      but every platform imports AddConfigEntryEntitiesCallback, 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.py and 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 .deb goes on
      Debian 12 and the .rpm on 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. .deb and .rpm for 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-rc channel 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, because uv leaves 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, MotorCommandError and the
      allow_motor argument 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 using select.<robot>_clean_cycle_wait_time must
      move to number.<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.

    • isPanelSleepMode and 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 as 0x00-0x7F with 123 of those 128
      addresses mapped. unitPowerType is pinned (0 = mains, 1 = battery) and
      isUSBPowerOn turns out to mean mains present, not USB. 0x02A10000 returns
      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
  • v0.2.0-rc.4 8adaa033ad

    v0.2.0-rc.4
    Some checks failed
    CI / Lint, type-check, test (library, 3.11.0) (push) Successful in 33s
    CI / Lint, type-check, test (library, 3.14) (push) Successful in 31s
    CI / Shell scripts and documentation links (push) Successful in 19s
    CI / Test the dependency floors the package advertises (push) Successful in 29s
    Publish / Publish to PyPI (push) Successful in 30s
    CI / Type-check + test the integration (push) Successful in 2m52s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelled
    Publish / Update the Homebrew tap formula (stable or -rc per tag) (push) Has been cancelled
    Publish / Bridge the macOS .pkg → NAS (push) Has been cancelled
    Pre-release

    Cody Bryant released this 2026-08-10 21:16:50 -07:00 | 145 commits to main since this release

    Thanks 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 gains empty-cycle and power,
      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
      skips quality_scale.yaml for 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 0x0B annunciator value is named (bonnet, cat, cycle, ready, night
      light, reset), which closes the "random housekeeping chatter" question.
    • robotStatus 10 is the clean cycle, on every firmware. The old map called
      it a cat pause; a narrated live cycle disproved that. Also adds 5, 6/7,
      25, the power-up states 1/2/3, the boot cycle 13, and the filter
      wizard 14. 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
      publish unknown_4 mid-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_removed is 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 local mb* 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 auto works. The name-to-number map was
      passed as dict.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, and link.apply_setting — the command line's own copy of the
      write-verify-retry loop — was barely exercised. Adds the destructive-action
      confirmation prompts (including that power has no --yes to skip it), the
      raw send guard 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%, with safety.py at 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 that strings.json and the shipped English still match, that every
      entity resolves to a translated name, that quality_scale.yaml still 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.json said 2025.2.0,
      but every platform imports AddConfigEntryEntitiesCallback, 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.py and 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 .deb goes on
      Debian 12 and the .rpm on 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. .deb and .rpm for 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-rc channel 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, because uv leaves 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, MotorCommandError and the
      allow_motor argument 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 using select.<robot>_clean_cycle_wait_time must
      move to number.<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.

    • isPanelSleepMode and 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 as 0x00-0x7F with 123 of those 128
      addresses mapped. unitPowerType is pinned (0 = mains, 1 = battery) and
      isUSBPowerOn turns out to mean mains present, not USB. 0x02A10000 returns
      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
  • v0.2.0-rc.3 41e652ee27

    v0.2.0-rc.3
    Some checks failed
    CI / Lint, type-check, test (library, 3.11) (push) Successful in 27s
    CI / Lint, type-check, test (library, 3.14) (push) Successful in 33s
    Publish / Publish to PyPI (push) Successful in 29s
    CI / Type-check + test the integration (push) Successful in 2m12s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelled
    Publish / Bridge the macOS .pkg → NAS (push) Has been cancelled
    Pre-release

    Cody Bryant released this 2026-08-09 23:23:19 -07:00 | 170 commits to main since this release

    Most 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 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 gains empty-cycle and power,
      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 0x0B annunciator value is named (bonnet, cat, cycle, ready, night
      light, reset), which closes the "random housekeeping chatter" question.
    • robotStatus 10 is the clean cycle, on every firmware. The old map called
      it a cat pause; a narrated live cycle disproved that. Also adds 5, 6/7,
      25, the power-up states 1/2/3, the boot cycle 13, and the filter
      wizard 14. 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
      publish unknown_4 mid-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_removed is 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 local mb* 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, MotorCommandError and the
      allow_motor argument 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 using select.<robot>_clean_cycle_wait_time must
      move to number.<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.
    • isPanelSleepMode and 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 as 0x00-0x7F with 123 of those 128
      addresses mapped. unitPowerType is pinned (0 = mains, 1 = battery) and
      isUSBPowerOn turns out to mean mains present, not USB. 0x02A10000 returns
      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
  • v0.2.0-rc.2 7229ac039f

    v0.2.0-rc.2
    Some checks failed
    CI / Lint, type-check, test (library, 3.11) (push) Successful in 27s
    CI / Lint, type-check, test (library, 3.14) (push) Successful in 29s
    Publish / Publish to PyPI (push) Successful in 36s
    CI / Type-check + test the integration (push) Successful in 2m5s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Has been cancelled
    Publish / Bridge the macOS .pkg → NAS (push) Has been cancelled
    Pre-release

    Cody Bryant released this 2026-08-09 11:06:49 -07:00 | 186 commits to main since this release

    Most 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 — so cat_weight was permanently null. New library events.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
      (register 0x56 — 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. litterLevel is 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

    • robotStatus 10 is the clean cycle, on every firmware. This table has said
      10 = cat/weight pause and 13 = 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 holds robotStatus at 10
      for the entire cycle with catDetect at 0 throughout, and 13 has never
      appeared on either captured firmware. Also adds 5, 6/7 and 25.
    • 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_4 mid-cycle. robotCycleState 4 is 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
      keyUsage extension — 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 local mb* 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 as unknown. Breaking:
      automations referencing select.<robot>_clean_cycle_wait_time must 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
  • v0.2.0-rc.1 7e2d84e2fb

    v0.2.0-rc.1
    Some checks failed
    CI / Lint, type-check, test (library, 3.11) (push) Successful in 29s
    CI / Lint, type-check, test (library, 3.14) (push) Successful in 28s
    Publish / Publish to PyPI (push) Successful in 28s
    CI / Type-check + test the integration (push) Successful in 2m0s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Failing after 35s
    Publish / Bridge the macOS .pkg → NAS (push) Has been skipped
    Pre-release

    Cody Bryant released this 2026-08-09 01:29:07 -07:00 | 192 commits to main since this release

    Most 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 — so cat_weight was permanently null. New library events.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
      (register 0x56 — 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. litterLevel is 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

    • robotStatus 10 is the clean cycle, on every firmware. This table has said
      10 = cat/weight pause and 13 = 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 holds robotStatus at 10
      for the entire cycle with catDetect at 0 throughout, and 13 has never
      appeared on either captured firmware. Also adds 5, 6/7 and 25.
    • 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_4 mid-cycle. robotCycleState 4 is 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
      keyUsage extension — 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 local mb* 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 as unknown. Breaking:
      automations referencing select.<robot>_clean_cycle_wait_time must 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
  • v0.1.3 6c60d2d179

    v0.1.3
    All checks were successful
    Publish / Publish to PyPI (push) Successful in 53s
    CI / Type-check + test the integration (push) Successful in 4m12s
    CI / Lint, type-check, test (library) (push) Successful in 50s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 1m14s
    Publish / Bridge the macOS .pkg → NAS (push) Successful in 8s
    Stable

    Cody Bryant released this 2026-07-01 22:03:06 -07:00 | 233 commits to main since this release

    Added

    • BLE provisioning now fails on non-LR4 devices. The serial is validated up
      front (must be the LR4… 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 --address path that
      previously accepted an arbitrary BLE address.
    Downloads
  • v0.1.2 49d96ea9fb

    v0.1.2
    All checks were successful
    Publish / Publish to PyPI (push) Successful in 51s
    CI / Type-check + test the integration (push) Successful in 4m5s
    CI / Lint, type-check, test (library) (push) Successful in 47s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 1m33s
    Publish / Bridge the macOS .pkg → NAS (push) Successful in 8s
    Stable

    Cody Bryant released this 2026-06-29 09:06:17 -07:00 | 236 commits to main since this release

    Removed

    • The clean-cycle command — the Home Assistant button, the CLI clean-cycle
      subcommand, and commands.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

    • 0xA3 reclassified to never-send. It's the reset / main-board-OTA
      orchestrator, not a motor command; the safety guard now refuses it unconditionally
      alongside 0xA4 / 0xAC / 0xAD. No motor command is exposed until a real
      cleanCycle trigger is recovered — the MOTOR / allow_motor gate stays wired but
      empty.
    • Documented the action-command hunt in docs/reverse-engineering.md: the
      0xA3 correction, 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
  • v0.1.1 5b4125955d

    v0.1.1
    All checks were successful
    Publish / Publish to PyPI (push) Successful in 26s
    CI / Type-check + test the integration (push) Successful in 1m52s
    CI / Lint, type-check, test (library) (push) Successful in 29s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 42s
    Publish / Bridge the macOS .pkg → NAS (push) Successful in 5s
    Stable

    Cody Bryant released this 2026-06-28 20:27:30 -07:00 | 239 commits to main since this release

    Fixed

    • The signed macOS installer (Apple Silicon + Intel) now builds and ships;
      0.1.0 targeted the since-retired macos-13 runner image, so that release
      shipped without the macOS .pkg.
    Downloads
  • v0.1.0 82569f00f3

    v0.1.0
    All checks were successful
    CI / Lint, type-check, test (library) (push) Successful in 25s
    Publish / Publish to PyPI (push) Successful in 25s
    CI / Type-check + test the integration (push) Successful in 1m46s
    Publish / Create releases + Linux binary (Forgejo, NAS, GitHub) (push) Successful in 41s
    Publish / Bridge the macOS .pkg → NAS (push) Successful in 30m9s
    Stable

    Cody Bryant released this 2026-06-28 18:49:26 -07:00 | 244 commits to main since this release

    Added

    • whiskerless Python 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 LitterRobot4Client with 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 (no protoc build step).
      • A safety guard that refuses brick/reset-class commands (0xAC/0xA4/0xAD)
        unconditionally and gates motor / untraced commands.
    • whiskerless CLIprovision, monitor, state, read, set,
      clean-cycle, and a guarded raw send.
    • 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