feat(homeassistant): waiting updates, backup age, and updates installed after a confirmed backup (#6355)
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:
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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")),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user