Skip to content

fix: BusyBarClient.set_busy_simple sends a body the device rejects - #11

Merged
sumitake merged 1 commit into
mainfrom
dev/claude/fix-set-busy
Aug 4, 2026
Merged

fix: BusyBarClient.set_busy_simple sends a body the device rejects#11
sumitake merged 1 commit into
mainfrom
dev/claude/fix-set-busy

Conversation

@sumitake

@sumitake sumitake commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Root cause

BusyBarClient.set_busy_simple (src/busybar/client.py) sent PUT /api/busy/snapshot with the BusySnapshotSimple fields flat at the top level -- the literal shape described by the device's own /openapi.yaml (BusySnapshot = the discriminated snapshot variant merged via allOf with a required busy_bar_settings, all flattened). Against a live device this body gets HTTP 400 "Failed to parse snapshot" on every call. The caller (calendar_countdown/main.py's run_once, the auto_busy=true branch) never checks set_busy_simple's bool return, so the feature silently did nothing.

Corrected shape (verified empirically against the live device, not guessed)

{
  "snapshot": {
    "type": "SIMPLE",
    "card_id": "00000000-0000-0000-0000-000000000000",
    "time_left_ms": 90000,
    "is_paused": false
  },
  "snapshot_timestamp_ms": 1785829020000
}

The snapshot variant is nested under a "snapshot" key, sibling to a top-level "snapshot_timestamp_ms" -- mirroring get_busy()'s own GET response shape -- and busy_bar_settings is not sent at all on this write path, despite the spec marking it required (the device fills its own default busy_bar_settings in, visible on the next GET).

A second finding surfaced during probing: snapshot_timestamp_ms must be a genuinely current timestamp. Reusing a stale/placeholder value (e.g. a timestamp copied from a prior GET) still returns HTTP 200 but silently no-ops the write -- the busy state does not actually change. set_busy_simple now sends int(time.time() * 1000).

Evidence

Direct on-device probing (device at 10.0.4.20, firmware 1.1.1):

$ curl -X PUT .../api/busy/snapshot -d '{"snapshot":{"type":"SIMPLE",...},"snapshot_timestamp_ms":<stale>}'
{"result":"OK"}  HTTP 200   <- but GET afterward still shows NOT_STARTED (stale timestamp silently ignored)

$ curl -X PUT .../api/busy/snapshot -d '{"snapshot":{"type":"SIMPLE",...},"snapshot_timestamp_ms":<fresh now-ms>}'
{"result":"OK"}  HTTP 200
$ curl .../api/busy/snapshot
{"snapshot":{"type":"SIMPLE","card_id":"00000000-...","is_paused":false,"time_left_ms":30000,
             "busy_bar_settings":{"theme":"busy","show_work_phase_only":false,"trigger_smart_home":true}},
 "snapshot_timestamp_ms":1785829020000}

Then ran the real calendar_countdown.main.run_once against the real device with auto_busy=true and a synthetic in-progress CalEvent (fake fetch; draw/clear no-op'd via a thin proxy to avoid touching the display, per this task's device-phase constraints):

summary: drew active 'synthetic verify event' -> drawn
GET snapshot -> type=SIMPLE, is_paused=False, time_left_ms=60000
PASS: auto_busy started a real SIMPLE session matching the synthetic event's remaining time

Device was returned to NOT_STARTED afterward (confirmed via GET) and left idle.

Tests

tests/test_client.py::test_set_busy_simple_payload rewritten to assert the nested body shape (mocking time.time for a deterministic snapshot_timestamp_ms), plus a new test_set_busy_simple_false_on_400 regression guard for the failure path.

$ TZ=UTC uv run pytest -q
........................................................................ [ 32%]
........................................................................ [ 64%]
........................................................................ [ 96%]
.......                                                                  [100%]
223 passed in 0.10s

set_busy_simple's public signature is unchanged.

The device's /openapi.yaml documents PUT /api/busy/snapshot's
BusySnapshot body as the discriminated snapshot variant merged (via
allOf) with a required busy_bar_settings, sent flat -- the shape
set_busy_simple sent. Against a live device this flat body gets HTTP
400 "Failed to parse snapshot" on every call, silently breaking
calendar_countdown's auto_busy=true feature (it never surfaces the
failure -- set_busy_simple's bool return is not checked by the
caller).

Empirically the firmware instead wants the snapshot variant nested
under a "snapshot" key, sibling to a top-level
"snapshot_timestamp_ms" -- mirroring get_busy()'s own GET response
shape -- and does NOT want busy_bar_settings on this write path at
all. Also found on-device: snapshot_timestamp_ms must be a genuinely
current timestamp; a stale/placeholder value still returns 200 but
silently no-ops the write, so set_busy_simple now sends
time.time()-derived "now" rather than a fixed value.

Verified on-device: the corrected body returns 200 and a subsequent
GET /api/busy/snapshot shows a real active SIMPLE session; ran
calendar_countdown.main.run_once with auto_busy=true and a synthetic
in-progress event against the real device (draw/clear no-op'd to
avoid touching the display) and confirmed the session actually
starts. Device left in NOT_STARTED afterward.

Public signature of set_busy_simple is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sumitake
sumitake merged commit 4c34517 into main Aug 4, 2026
4 checks passed
@sumitake
sumitake deleted the dev/claude/fix-set-busy branch August 4, 2026 07:41

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df82e512e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/busybar/client.py
Comment on lines +87 to +89
"snapshot": {"type": "SIMPLE", "card_id": NULL_CARD_ID,
"time_left_ms": time_left_ms, "is_paused": False},
"snapshot_timestamp_ms": int(time.time() * 1000),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize the nested busy snapshot before writing

When auto_busy is enabled during an active event, the live GET shape documented here is {"snapshot": {"type": ...}, ...}, but calendar_countdown.run_once still checks busy.get("type"). It therefore treats every existing SIMPLE or other active session as absent, and now that this PUT payload is accepted, overwrites that session on every poll. Update get_busy() to return the nested snapshot or change the caller to inspect busy["snapshot"]["type"] before enabling these successful writes.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant