Phase C of the program viewer. Replaces the "panel coming soon" stub
with a real Lit-based side panel that consumes the Phase-B websocket
commands.
Layout (top to bottom):
* Header — title + total/filtered count
* Filter bar — search box (substring match), trigger-type chips
(TIMED / EVENT / YEARLY / WHEN / AT / EVERY / REMARK), a clearable
"filtering on <ref>" pill when an entity filter is active
* Two-column body: program list on the left, slide-in detail panel
on the right. Collapses to single-column on `narrow` view.
The program list renders one row per program (or per chain head, for
clausal multi-record programs). Each row carries the slot number,
the rendered one-line summary token stream, and meta pills for
trigger type / condition count / multi-action count.
The detail panel renders the full structured-English token stream
inside a styled <pre>. A "Fire now" button calls
``omni_pca/programs/fire`` over the wire — the panel actually
runs the program. For chain detail the spanned slot range is shown
underneath.
REF tokens are rendered as `<button>` elements that click to filter
the list to "programs that mention this entity" — the most useful
navigational affordance for the "why is this happening?" use case.
Live-state badges (SECURE / NOT READY / ON 60% / Away / 72°F / …) are
appended to REF tokens via the Phase-B coordinator-backed
StateResolver. The panel polls ``programs/list`` every 5 seconds to
refresh badges; switching to push-event subscriptions is a follow-up
when polling overhead becomes visible.
Theming uses HA's standard CSS variables (--primary-color,
--card-background-color, --divider-color, etc.) so the panel inherits
the user's HA theme automatically.
Build pipeline:
* TypeScript source under ``custom_components/omni_pca/frontend/src/``
* esbuild bundles entry → ESM in one self-contained file
* Output at ``custom_components/omni_pca/www/panel.js`` (~34 KB
minified) is committed so end-users don't need Node installed
* ``npm run watch`` for HA-dev-time iteration
* tsconfig has strict mode + noUnusedLocals; bundle currently
type-checks clean
Manifest declares deps on ``http`` and ``websocket_api``; ``frontend``
and ``panel_custom`` are loaded opportunistically (they require
``hass_frontend`` which the test harness doesn't ship — keeping them
out of the manifest deps keeps tests green).
Full suite: 634 passed, 1 skipped (no test changes; the integration
side hasn't moved since Phase B).
First half of the autonomous program-execution engine. Two phases land
together because Phase 1 was pure scaffolding (Clock + classifier)
and made little sense in isolation.
Phase 1 — engine foundation:
* Clock protocol with RealClock (wall time + asyncio.sleep) and
FakeClock (manual advance, no real waiting; sleepers wake in
chronological order on advance_to).
* classify(programs) splits a Program tuple into timed / event /
yearly / clausal-head buckets, dropping FREE / REMARK / unknown
records and the AND/OR/THEN clausal continuations (those are
reached by walking forward from each WHEN/AT/EVERY head, not by
classification).
* ProgramEngine class with start / stop lifecycle (idempotent +
context-manager), per-program asyncio task list, _EngineMetrics
counters.
Phase 2 — TIMED programs actually run:
* _next_absolute_fire(now, program) computes the next datetime at
which a TIMED program with TimeKind.ABSOLUTE should fire, given
its hour/minute/days mask. Walks forward up to 8 days; returns
None for empty Days mask (program is effectively disabled).
* Each TIMED program gets its own asyncio task running
sleep-until-next-fire / fire / loop. Firing dispatches the
4-byte Command wire payload (cmd / par / pr2) through
MockPanel._handle_command — same code path the v2 Command opcode
uses, so a TIMED program turning on a unit produces identical
state to a client sending the equivalent Command.
* astral added as an [engine] optional dependency, pinned to 2.2
for HA compat (HA itself pins astral==2.2). Library wired up but
not yet consumed — sunrise/sunset support lands in Phase 3.
Tests (28 new):
* RealClock and FakeClock behaviour incl. chronological wake order.
* classify against each ProgramType, unknown values, empty input.
* Engine lifecycle (idempotent start/stop, context manager,
malformed-record tolerance).
* End-to-end: TIMED UNIT_ON program fires at the right Monday 06:00,
loops correctly across weeks, never fires outside its Days mask,
ignores programs with empty Days mask.
Full suite: 527 passed, 1 skipped (up from 499).
Final cross-reference round, covering the remaining files where wire
bytes have a user- or installer-facing counterpart:
v1/messages.py
New Cross-references block: SETUP ZONES + SETUP TEMPERATURES for the
fields the parsers' raw bytes ultimately come from, and APPENDIX C
for what each synthesized index means on hardware (unit 257+ =
expansion-enclosure outputs, 393+ = panel flags).
models.ZoneStatus
Status-byte bit-layout doc now also points at the Owner's Manual
CONTROL chapter's "View Zone Status" keypad screen -- same Secure /
Not Ready / Trouble / Tamper labels.
models.UnitStatus
State-byte semantics doc references the Owner's Manual CONTROL
chapter for the user-side actions (All On/All Off/Scene/Bright/Dim)
that drive units into each of these states.
mock_panel.py
Notes that the mock's plausible-but-arbitrary RequestProperties /
RequestStatus responses correspond on real hardware to what an
installer typed into INSTALLER SETUP. Production fixtures should
pre-seed MockPanel state to match a known SETUP configuration.
uv.lock
Catches up the project's own entry to omni-pca 2026.5.11 (was
pinned to 2026.5.10 from the previous lock generation).
No code changes; 387 tests still pass.
Pytest harness (in-process HA + MockPanel)
==========================================
pyproject.toml — bumps requires-python to 3.14.2 to align with HA 2026.5.x
which is what pytest-homeassistant-custom-component pins. Dev group 'ha'
pulls the harness; .python-version updated to 3.14.
src/omni_pca/mock_panel.py — Thermostat (6) and Button (3) RequestProperties
handlers added (previous commit). Without these the HA coordinator's
discovery walk produced empty thermostat/button dicts.
custom_components/omni_pca/services.py — fix CONF_ENTRY_ID import: HA
exports it as ATTR_CONFIG_ENTRY_ID, not CONF_ENTRY_ID. Aliased on import.
tests/conftest.py — re-enables sockets globally (the HA harness installs
pytest_socket which otherwise blocks our network e2e tests).
tests/ha_integration/ — new directory with full HA boot harness:
conftest.py:
- autouse enable_custom_integrations so HA loads our component
- autouse expected_lingering_tasks=True (background event listener)
- autouse _short_scan_interval (1s instead of 30s for fast tests)
- panel fixture: MockPanel on a random localhost port for each test
- configured_panel fixture: builds a MockConfigEntry, runs setup,
yields, then unloads on teardown so the coordinator's reader task
and OmniClient socket close cleanly (otherwise verify_cleanup hangs)
test_setup.py — 12 tests:
- integration loads + system_info populated
- alarm_control_panel/light/switch/climate/button/event/binary_sensor
entities materialise per platform
- unload_entry tears down cleanly
- turning a light on via HA service updates the mock state
- arming via HA service with the right code transitions the area
- arming with wrong code keeps the area disarmed and surfaces error
Total: 351 passed, 1 skipped (PCA fixture). Ruff clean across src/ tests/
custom_components/. The 12 HA integration tests run in <1s end-to-end —
they boot HA in-process, drive the config flow, exercise services, and
verify state mutations on the mock side.
Docker dev stack (manual smoke / screenshots)
=============================================
dev/docker-compose.yml — HA 2026.5 container + MockPanel sidecar.
dev/run_mock_panel.py — long-running mock with a populated state
(5 zones, 4 units, 2 areas, 2 thermostats, 3 buttons, codes 1234/5678).
dev/Makefile — make dev-up / dev-logs / dev-down / dev-mock / dev-reset.
dev/README.md — onboarding walkthrough (host=host.docker.internal,
port=14369, controller_key=000102030405060708090a0b0c0d0e0f).
.gitignore — adds ha-config/ so the persisted HA state from the dev
stack doesn't get committed.