panel: trigger initial loadList from discover, prefer loaded entries
Some checks are pending
Validate / HACS validation (push) Waiting to run
Validate / Hassfest (push) Waiting to run

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.
This commit is contained in:
Ryan Malloy 2026-05-16 17:48:17 -06:00
parent 14d16a5a4c
commit 4781f4d276
13 changed files with 89 additions and 4 deletions

View File

@ -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)}`;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -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