Don't feed a {"ok":false} no-data beat to the rate ring - #151
Open
TB1982 wants to merge 1 commit into
Open
Conversation
The daemon writes `{"ok": False}` — that key and nothing else — when it has
no usable token (`claude_usage_daemon.py`, the "No usable token; signalling
no-data to device" branch). `parse_json` fills the rest from defaults, so
`session_pct` arrives as 0.0f, and `main.cpp` handed that straight to
`usage_rate_sample()` before anything looked at `ok`.
`usage_rate_sample()` reads a drop of more than five points as a 5-hour
refill: it clears the ring and returns true. So one no-data beat, after any
reading above 5%, threw away the rate history and reported a session reset
— the device rings the chime to say the quota came back at the moment the
daemon stopped being able to ask about it.
`ui_update()` already guards on `ok`, with the comment "fall through to
idle, keep last numbers", so the intent that these beats carry no readings
is established. But the sampler runs at line 375 and `ui_update` at 390:
the guard sat downstream of the damage. Moving the sampler inside the same
condition is the whole change.
`test_freeride.py::test_freeride_autherror_emits_no_data_beat` already
covers the daemon half — the beat is sent correctly. Nothing covered what
the firmware did with it.
Also adds `daemon/requirements-dev.txt`. The runtime deps are declared per
platform (install-mac.sh pins bleak/httpx inline, requirements-windows.txt
covers the tray), but the test extras were nowhere, so running the suite
means discovering bleak, httpx, Pillow and pytest one ModuleNotFoundError
at a time. pystray is deliberately excluded: tray_windows.py imports it
inside main() and test_windows_tray.py documents that the tests never touch
the top-level module, which would want a GUI toolkit on Linux.
Verified: builds on waveshare_amoled_216. A fresh venv installed from the
new manifest alone runs the suite green — 119 passed, 2 skipped, the two
skips being the Linux/WSL-only pair that can't run on macOS by their own
markers. Not exercised on hardware: reproducing it needs the daemon's
no-token path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A
{"ok":false}beat makes the device announce that the 5-hour quota came back, at the moment the daemon stops being able to ask about it.The chain:
claude_usage_daemon.pyhas no usable token, so it writes{"ok": False}— that key and nothing else.parse_jsonfills the rest from defaults, sosession_pctarrives as0.0ffromdoc["s"] | 0.0frather than as a reading.main.cpphanded that tousage_rate_sample()before anything looked atok.usage_rate_sample()treats a drop of more than five points as a session refill — it clears the ring and returnstrue.ui_update()already guards onok, with the comment "fall through to idle, keep last numbers" — the intent that these beats carry no readings is established. But the sampler runs at line 375 andui_update()at 390, so the guard sat downstream of the damage.Moving the sampler inside the same condition is the whole fix. No behaviour changes for a beat that has real numbers.
test_freeride.py::test_freeride_autherror_emits_no_data_beatalready covers the daemon half; the beat is sent exactly as intended. Nothing covered what the firmware did on receipt.Also:
daemon/requirements-dev.txtRuntime deps are declared per platform and this doesn't touch that —
install-mac.shpinsbleak/httpxinline,requirements-windows.txtcovers the tray build. But the test extras weren't declared anywhere, so runningpytest daemon/testsmeans discoveringbleak,httpx,PillowandpytestoneModuleNotFoundErrorat a time. That's how I found them.pystrayis deliberately excluded.tray_windows.pyimports it insidemain(), andtest_windows_tray.pysays in its own docstring that the tests exercise the handlers without importing the top-level module — which would want a GUI toolkit on Linux. Adding it would make every contributor pay for that.Plus a short README section, since a manifest nobody can find is half a fix.
Verification
waveshare_amoled_216.usage_rate.cpp'ssession_pct + 5.0f < ring[latest].pct.Happy to split the manifest out into its own PR if you'd rather keep the fix by itself.