"""Repair issues for House Plan. The check runs at entry setup AND after every config save (ws_config_set), so a plan file that goes missing — or gets re-uploaded — is reflected in the Repairs UI without waiting for a restart. """ from __future__ import annotations from pathlib import Path from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from .const import CONTENT_URL, DOMAIN, PLANS_DIR, PLANS_URL from .store import HouseplanConfigEntry async def async_check_plan_files(hass: HomeAssistant, entry: HouseplanConfigEntry) -> None: """Raise an issue for every space whose plan file is missing on disk.""" cfg_raw = await entry.runtime_data.config_store.async_load() or {} spaces = cfg_raw.get("config", {}).get("spaces", []) plans_dir = Path(hass.config.path(PLANS_DIR)) def _missing() -> list[tuple[str, str]]: res = [] for sp in spaces: url = sp.get("plan_url") or "" # both the legacy static URL and the authenticated content URL prefix = None if url.startswith(PLANS_URL + "/"): prefix = PLANS_URL + "/" elif url.startswith(CONTENT_URL + "/plans/_/"): prefix = CONTENT_URL + "/plans/_/" if prefix is None: continue # external/legacy URL — not ours to verify fname = url[len(prefix) :].split("?", 1)[0] if not (plans_dir / fname).is_file(): res.append((sp.get("id", "?"), fname)) return res missing = await hass.async_add_executor_job(_missing) broken = set() for space_id, fname in missing: broken.add(space_id) ir.async_create_issue( hass, DOMAIN, f"broken_plan_{space_id}", is_fixable=False, severity=ir.IssueSeverity.WARNING, translation_key="broken_plan", translation_placeholders={"space": space_id, "file": fname}, ) # Clear stale issues. Iterating the CURRENT spaces could only ever clear # issues for spaces that still exist, so deleting or renaming a space with a # missing plan left its warning in Repairs forever, with nothing left to fix # it (HP-1454-09). Enumerate what we actually published instead. registry = ir.async_get(hass) stale = [ issue_id for (domain, issue_id) in list(registry.issues) if domain == DOMAIN and issue_id.startswith("broken_plan_") and issue_id[len("broken_plan_") :] not in broken ] for issue_id in stale: ir.async_delete_issue(hass, DOMAIN, issue_id)