panel: trigger initial loadList from discover, prefer loaded entries
Two bugs surfaced when smoke-testing against a real OmniPro II: 1. Empty list after page load. _discoverViaList ran fire-and-forget; connectedCallback then synchronously checked _entryId (still null because await hadn't resolved) and skipped _loadList. The panel rendered "No programs match the current filters" forever — until the next 5-second poll tick, which never fires because _startRefreshTimer was also gated on the same null check. Fix: have _discoverViaList itself trigger _loadList and _startRefreshTimer after _entryId lands. The connectedCallback / updated paths can stay gated on _entryId; the discover path now takes ownership of "do the initial load too." 2. Dev installs with both a working entry and a setup_retry entry (mock container down, real panel up) had the panel pick the setup_retry one first and surface "panel not configured" on every call. Fix: prefer entries with state === "loaded" in the discover step, falling back to first entry only when none are loaded. Also: screenshot.py drops the seed-via-WS step (was unsafe — would write Programs to whatever entry is loaded, including real panels). Updates the in-page click helpers to walk the shadow DOM recursively instead of hardcoding HA's host-element path, so detail/editor screenshots work on the actual depth-8 element location. Smoke test against real panel: 154 programs render correctly with structured English, BEDTIME / OPEN BIG GAR / Zone 133 events all decoded, B. GAR MAN DOOR [SECURE] live-state badge visible. Detail panel + editor mode both function end-to-end.
@ -137,18 +137,37 @@ export class OmniPanelPrograms extends LitElement {
|
||||
// Best-effort: walk known config entries via HA's standard
|
||||
// config_entries/get command. If that fails we surface a friendly
|
||||
// error pointing at integration setup.
|
||||
//
|
||||
// NOTE: this runs fire-and-forget from connectedCallback (and from
|
||||
// the hass-update path), so we need to kick off the initial list
|
||||
// *here*, after _entryId lands. Earlier versions checked _entryId
|
||||
// synchronously right after calling discover, which always saw
|
||||
// null and silently skipped loadList — the panel rendered "no
|
||||
// programs" forever until the first manual refresh.
|
||||
try {
|
||||
const entries = await this.hass.connection.sendMessagePromise<
|
||||
Array<{ entry_id: string; domain: string; title: string }>
|
||||
Array<{ entry_id: string; domain: string; title: string; state?: string }>
|
||||
>({ type: "config_entries/get" });
|
||||
const ours = entries.filter((e) => e.domain === "omni_pca");
|
||||
if (ours.length === 0) {
|
||||
this._error = "No Omni panel configured. Add one via Settings → Devices & Services.";
|
||||
return;
|
||||
}
|
||||
// First entry wins for v1; multi-panel selector is a follow-up.
|
||||
this._entryId = ours[0].entry_id;
|
||||
// Prefer entries that are actually loaded — a config entry in
|
||||
// setup_retry or migration_error has no live coordinator, and
|
||||
// the websocket commands return "panel not configured" against
|
||||
// it. Multi-loaded-panel installs still pick the first loaded
|
||||
// one; a multi-panel selector is a follow-up.
|
||||
const loaded = ours.find((e) => e.state === "loaded");
|
||||
this._entryId = (loaded ?? ours[0]).entry_id;
|
||||
this._error = null;
|
||||
// Kick off the initial list + start the live-state refresh timer.
|
||||
// Both are safe to call from here regardless of whether the
|
||||
// caller (connectedCallback / updated) also expected to start
|
||||
// them; _loadList is reentrant-safe and _startRefreshTimer is
|
||||
// idempotent.
|
||||
void this._loadList();
|
||||
this._startRefreshTimer();
|
||||
} catch (err) {
|
||||
this._error = `Could not discover panels: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
|
||||
BIN
dev/artifacts/screenshots/2026-05-16/01-overview.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/02-integrations-list.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/03-omni-pca-config.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/04-panel-device.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/05-entities-omni.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/06-developer-states.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/07-side-panel-empty.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/08-side-panel-programs.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/09-side-panel-detail.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
dev/artifacts/screenshots/2026-05-16/10-side-panel-editor.png
Normal file
|
After Width: | Height: | Size: 348 KiB |
@ -298,6 +298,72 @@ async def _take_screenshots(ha_url: str, token: str, outdir: Path) -> list[Path]
|
||||
await shot("06-developer-states.png",
|
||||
"/developer-tools/state", wait_secs=4.0)
|
||||
|
||||
# The side panel registered by panel_custom (websocket.py:
|
||||
# async_register_side_panel). If pointed at a real panel the
|
||||
# program list is whatever the homeowner has authored; against
|
||||
# the mock it's whatever ``run_mock_panel.py`` seeded. We
|
||||
# deliberately do NOT write programs from here because the
|
||||
# screenshot script may be aimed at real hardware.
|
||||
await shot("08-side-panel-programs.png",
|
||||
"/omni-panel-programs", wait_secs=6.0)
|
||||
# Helper: locate the omni-panel-programs element regardless of
|
||||
# what shadow-DOM path HA's panel host wraps it in. Recursive
|
||||
# walk because partial-panel-resolver / hui-view / etc. can
|
||||
# vary between HA versions.
|
||||
find_panel_js = """
|
||||
(() => {
|
||||
function find(root, depth=0) {
|
||||
if (!root || depth > 15) return null;
|
||||
if (root.tagName === 'OMNI-PANEL-PROGRAMS') return root;
|
||||
for (const k of Array.from(root.children || [])) {
|
||||
const r = find(k, depth+1);
|
||||
if (r) return r;
|
||||
}
|
||||
if (root.shadowRoot) {
|
||||
const r = find(root.shadowRoot, depth+1);
|
||||
if (r) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return find(document.body);
|
||||
})()
|
||||
"""
|
||||
|
||||
# Click into the first program row to capture the detail panel.
|
||||
try:
|
||||
await page.evaluate(f"""() => {{
|
||||
const panel = {find_panel_js};
|
||||
if (!panel) {{ console.warn('omni panel not found'); return; }}
|
||||
const row = panel.shadowRoot.querySelector('.row');
|
||||
if (row) row.click();
|
||||
}}""")
|
||||
await page.wait_for_timeout(800)
|
||||
except Exception as e:
|
||||
print(f" click-into-row warning: {e}")
|
||||
# Re-shoot WITHOUT a navigate (page.goto would reset selection).
|
||||
await page.screenshot(path=str(outdir / "09-side-panel-detail.png"),
|
||||
full_page=False)
|
||||
shots.append(outdir / "09-side-panel-detail.png")
|
||||
print(f" → 09-side-panel-detail.png (in-place)")
|
||||
|
||||
# Click "Edit" to capture the editor mode.
|
||||
try:
|
||||
await page.evaluate(f"""() => {{
|
||||
const panel = {find_panel_js};
|
||||
if (!panel) {{ console.warn('omni panel not found'); return; }}
|
||||
const buttons = panel.shadowRoot.querySelectorAll('.detail button');
|
||||
for (const b of buttons) {{
|
||||
if (b.textContent.trim() === 'Edit') {{ b.click(); break; }}
|
||||
}}
|
||||
}}""")
|
||||
await page.wait_for_timeout(800)
|
||||
except Exception as e:
|
||||
print(f" click-edit warning: {e}")
|
||||
await page.screenshot(path=str(outdir / "10-side-panel-editor.png"),
|
||||
full_page=False)
|
||||
shots.append(outdir / "10-side-panel-editor.png")
|
||||
print(f" → 10-side-panel-editor.png (in-place)")
|
||||
|
||||
await browser.close()
|
||||
return shots
|
||||
|
||||
|
||||