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.
87 lines
3.4 KiB
Python
87 lines
3.4 KiB
Python
"""v3 provider layer + config v2 tests.
|
|
|
|
Covers the framework M1 lands: config v1->v2 migration keeps Anthropic behaviour,
|
|
the provider registry hands out the right implementations, and ProviderStatus
|
|
maps to the compact BLE fields the firmware parser expects.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
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)."""
|
|
p = tmp_path / "config.json"
|
|
p.write_text(json.dumps(data), encoding="utf-8")
|
|
monkeypatch.setenv("CLAWDMETER_CONFIG", str(p))
|
|
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
|
|
return p
|
|
|
|
|
|
def test_v1_config_migrates_to_provider_defaults(tmp_path, monkeypatch):
|
|
# A v1 config has no providers/active_provider; load_config fills them so
|
|
# behaviour is unchanged — Anthropic enabled and active.
|
|
_write(tmp_path, monkeypatch, {
|
|
"version": 1, "ha": {"url": "", "token": "", "entities": []},
|
|
"buttons": [], "settings": {}})
|
|
cfg = config.load_config()
|
|
assert cfg["active_provider"] == "anthropic"
|
|
assert cfg["providers"]["anthropic"]["enabled"] is True
|
|
assert config.enabled_providers(cfg) == ["anthropic"]
|
|
|
|
|
|
def test_unknown_active_provider_falls_back(tmp_path, monkeypatch):
|
|
_write(tmp_path, monkeypatch, {"version": 2, "active_provider": "bogus", "providers": {}})
|
|
cfg = config.load_config()
|
|
assert cfg["active_provider"] == "anthropic"
|
|
assert config.active_provider_id(cfg) == "anthropic"
|
|
|
|
|
|
def test_display_order_covers_all_and_filters_unknown(tmp_path, monkeypatch):
|
|
_write(tmp_path, monkeypatch, {"version": 2, "display_order": ["zai", "bogus", "openai"]})
|
|
cfg = config.load_config()
|
|
# "bogus" dropped; the missing "anthropic" appended at the end.
|
|
assert cfg["display_order"] == ["zai", "openai", "anthropic"]
|
|
|
|
|
|
def test_enabled_providers_respects_order_and_flag(tmp_path, monkeypatch):
|
|
_write(tmp_path, monkeypatch, {
|
|
"version": 2, "display_order": ["zai", "openai", "anthropic"],
|
|
"providers": {"anthropic": {"enabled": True},
|
|
"openai": {"enabled": True},
|
|
"zai": {"enabled": False}}})
|
|
cfg = config.load_config()
|
|
assert config.enabled_providers(cfg) == ["openai", "anthropic"]
|
|
|
|
|
|
def test_registry_types():
|
|
a = get_provider("anthropic")
|
|
assert isinstance(a, AnthropicProvider) and a.id == "anthropic"
|
|
stub = get_provider("openai")
|
|
assert isinstance(stub, StubProvider)
|
|
assert stub.id == "openai" and stub.accent == "10a37f"
|
|
|
|
|
|
def test_stub_provider_poll_is_safe():
|
|
st = _run(get_provider("zai").poll())
|
|
assert isinstance(st, ProviderStatus)
|
|
assert st.ok is False
|
|
|
|
|
|
def test_provider_status_payload_keys():
|
|
st = ProviderStatus(s=50, w=10, ok=True, tokens_today=5, cost_cents_today=3)
|
|
p = st.to_payload()
|
|
assert set(p) == {"s", "sr", "w", "wr", "st", "ok", "tk", "to", "tc", "tn"}
|
|
assert p["s"] == 50 and p["tk"] == 5 and p["tc"] == 3 and p["ok"] is True
|