feat(homeassistant): waiting updates, backup age, and updates installed after a confirmed backup (#6355)
CI / lint (push) Failing after 2s
CI / unit (push) Failing after 1m42s
CI / integration (push) Successful in 1m51s
CI / publish (push) Skipped

A new plugin reads Home Assistant with a read-only token every 5 minutes and
shows which Core, OS and add-on updates are waiting and how old the last backup
is. Install takes a backup through a local-only webhook, confirms Home
Assistant's last-backup time moved, installs, and follows the update to its end,
through Home Assistant's and Steward's own restarts. One runs at a time. Each
ends as one timeline entry.

Steward holds nothing that can act on the house: the webhook's automation can
only take a backup or install an allowed update (System 231's ruling). A refused
token stops polling, as Home Assistant bans an address after a few refusals.

Milestone 576 steps 1 to 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 23:09:27 -04:00
co-authored by Claude Opus 5.5
parent 8b9b7fc11d
commit b2a6e92ed3
27 changed files with 1802 additions and 4 deletions
+4 -1
View File
@@ -214,6 +214,9 @@ ACTIONS = {
"prometheus.template_created": "added Prometheus widget template",
"prometheus.template_updated": "changed Prometheus widget template",
"prometheus.template_deleted": "deleted Prometheus widget template",
"homeassistant.connection_saved": "saved the Home Assistant connection",
"homeassistant.connection_removed": "removed the Home Assistant connection",
"homeassistant.update_started": "started a Home Assistant update",
"syslog.source_added": "added syslog source",
"syslog.source_updated": "changed syslog source",
"syslog.source_removed": "removed syslog source",
@@ -244,7 +247,7 @@ AREAS = (("auth.", "Sign-ins"), ("account.", "Accounts"), ("user.", "Users"), ("
("ansible.run", "Playbook runs"), ("ansible.", "Ansible (all)"),
("settings.", "Settings"), ("plugin.", "Plugins"), ("api_token.", "AI access"),
("docker.", "Docker"), ("snmp.", "SNMP"), ("prometheus.", "Prometheus"),
("syslog.", "Syslog"), ("backup.", "Backups"), ("timeline.", "Timeline"),
("syslog.", "Syslog"), ("homeassistant.", "Home Assistant"), ("backup.", "Backups"), ("timeline.", "Timeline"),
("ticket.", "Tickets"))
+6 -1
View File
@@ -29,6 +29,7 @@ KEPT: frozenset[str] = frozenset({
# Syslog senders: which were added, under what name, and which ignored.
"syslog_sources",
"syslog_patterns",
"homeassistant_connection",
})
# Rows that are measurements, logs, history or in-flight work: left out.
@@ -67,6 +68,7 @@ EXCLUDED: frozenset[str] = frozenset({
"unifi_events", "unifi_fw_rules", "unifi_networks", "unifi_port_forwards",
"unifi_speedtest_results", "unifi_wan_stats", "unifi_wlan_stats",
"syslog_lines",
"homeassistant_updates",
})
# ── What a check compares (#5823) ────────────────────────────────────────────
@@ -91,6 +93,8 @@ ACTIVITY: dict[str, frozenset[str]] = {
"snmp_devices": frozenset({"last_polled_at", "last_ok_at", "last_error", "missing"}),
"prometheus_targets": frozenset({"last_scraped_at", "last_ok_at", "last_error", "last_kept",
"last_dropped", "last_capped"}),
"homeassistant_connection": frozenset({"refused", "last_polled_at", "last_ok_at", "last_error",
"updates", "backup", "automation"}),
# What the sender last sent; its address, name, state and host are setup.
"syslog_sources": frozenset({"hostname_seen", "first_seen_at", "last_seen_at", "lines_total",
"last_severity", "last_line"}),
@@ -121,5 +125,6 @@ AREAS: tuple[tuple[str, tuple[str, ...]], ...] = (
("Ansible", ("ansible_targets", "ansible_groups", "ansible_target_groups", "ansible_schedules")),
("Dashboards", ("dashboards", "dashboard_widgets", "dashboard_share_tokens")),
("Plugins", ("snmp_devices", "prometheus_targets", "prometheus_widget_templates",
"unifi_known_clients", "syslog_sources", "syslog_patterns")),
"unifi_known_clients", "syslog_sources", "syslog_patterns",
"homeassistant_connection")),
)
+3
View File
@@ -298,6 +298,8 @@ def metric_value(name: str, value: Any) -> str:
# its underscores spaced (SNMP's OID labels are the operator's own words).
METRICS: dict[str, tuple[str, str]] = {
"is_up": ("Up or down", "whether the check passed"),
"backup_age_hours": ("Age of last backup (hours)", "hours since Home Assistant's last successful automatic backup"),
"updates_waiting": ("Updates waiting", "Core, OS and add-on updates Home Assistant has waiting"),
"is_healthy": ("Health check", "the container's own HEALTHCHECK result"),
"response_ms": ("Response time", "how long the check took"),
"ingest_posts": ("Agent posts per minute", "host-agent reports Steward took in the last minute"),
@@ -421,6 +423,7 @@ SOURCES: dict[str, str] = {
"http": "HTTP monitors", "steward": "Steward itself", "host_agent": "Host agent",
"docker": "Docker containers", "traefik": "Traefik routers", "unifi": "UniFi",
"snmp": "SNMP devices", "prometheus": "Prometheus targets", "syslog": "Syslog patterns",
"homeassistant": "Home Assistant",
}
+1
View File
@@ -181,6 +181,7 @@ DEFAULTS: dict[str, Any] = {
"plugin.snmp": {"enabled": True},
"plugin.prometheus": {"enabled": True},
"plugin.syslog": {"enabled": True},
"plugin.homeassistant": {"enabled": True},
# OIDC single-sign-on
"oidc.enabled": False,
"oidc.discovery_url": "",
+3 -1
View File
@@ -118,7 +118,7 @@ for _t in (
# A source not listed here is Monitoring.
_SOURCE_AREAS = {
"icmp": "network", "dns": "network", "tcp": "service", "http": "service",
"docker": "service", "prometheus": "service",
"docker": "service", "prometheus": "service", "homeassistant": "service",
"traefik": "network", "unifi": "network",
"snmp": "hardware",
}
@@ -154,6 +154,8 @@ _AUDIT_TYPES = (
("account.time_zone", None), ("account.email", None),
("host_agent.maintenance_plan", "steward.config"), ("host_agent.maintenance", None),
("docker.prune.", None),
# An update is on the timeline as itself, once it ends (homeassistant.timeline).
("homeassistant.update", None),
# A change recorded by hand is on the timeline as itself (timeline.changes).
("timeline.change", None),
# A ticket's opening, resolution and reopening are on the timeline as