v3 M1b: runtime per-provider theming (firmware) + accent push (daemon)

The watch recolors to the active provider's brand, live, and it's cheap to add
more providers later.

Firmware:
- theme.h: expose THEME_ACCENT_HEX; ui.cpp: COL_ACCENT becomes a runtime
  lv_color_t backed by a shared style (s_accent_style). Static accent widgets
  (launcher tile icons, usage status line, session tokens) attach the shared
  style, so ui_set_theme() recolors them instantly via lv_obj_report_style_change
  — no widget tracking, no recreation.
- ui_set_theme(pv, accent_rgb): swaps the accent + a small logo table keyed by
  provider id (fallback to the Claude mark until a provider ships a logo). Change-
  guarded, so the ~3s payload cadence doesn't churn.
- main.cpp: parse "pv" (id) + "ac" (0xRRGGBB) from the payload and apply.
- Adding a provider needs zero firmware color edits (accent is data-driven) and
  at most one logo-table row.

Daemon: push "ac" = active provider's brand accent alongside "pv".

Test fix: test_providers used asyncio.run(), which closed the suite's shared
event loop and broke every later get_event_loop() test (Py3.13) — mirror the
repo's _run() helper instead.

Verified on the 2.06: forcing an OpenAI-green theme recolored the live launcher
icons (report_style_change path). 216 builds clean; daemon suite 94 passed.
This commit is contained in:
wenil
2026-07-10 00:40:24 +03:00
parent 6e3f8e9084
commit 83bd2bed43
7 changed files with 83 additions and 7 deletions
+4
View File
@@ -915,6 +915,10 @@ async def connect_and_run(device, stop_event: asyncio.Event, tray_state=None) ->
auth_problem = status.auth_problem
cached = status.to_payload()
cached["pv"] = active_pv # which brand theme the watch wears
try: # brand accent (0xRRGGBB) for the theme
cached["ac"] = int(provider.accent, 16)
except (ValueError, TypeError):
pass
last_claude_poll = now
# Now Playing (Phase 5) — best-effort Windows media session, read every
+8 -1
View File
@@ -12,6 +12,13 @@ from daemon import config
from daemon.providers import get_provider, AnthropicProvider, StubProvider, ProviderStatus
def _run(coro):
# Reuse the suite's shared event loop (never closed) like the other test
# modules — asyncio.run() would close it and set the current loop to None,
# breaking every later test that calls get_event_loop() (Python 3.13).
return asyncio.get_event_loop().run_until_complete(coro)
def _write(tmp_path, monkeypatch, data: dict):
"""Point the config module at an isolated temp config (and LOCALAPPDATA, so
the legacy-ha_config migration can't reach the real machine's file)."""
@@ -67,7 +74,7 @@ def test_registry_types():
def test_stub_provider_poll_is_safe():
st = asyncio.run(get_provider("zai").poll())
st = _run(get_provider("zai").poll())
assert isinstance(st, ProviderStatus)
assert st.ok is False