From 4c57735dd942445e3e47850ce031a10acee7a844 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 1 Nov 2026 18:51:28 -0500 Subject: [PATCH] fix(homeassistant): storage reads used of total with its percentage, and an OS update is said to wait on a reboot (#6753, #6754) The tile shows Home Assistant's own used figure beside the total, so it matches Home Assistant's storage page. An OS update doesn't reboot the machine: Supervisor writes the image, reports the new version and raises a repair asking for a reboot. The confirmation, schedule warning, MCP docs and plugin docs now say so. Co-Authored-By: Claude Opus 5.5 --- docs/plugins/homeassistant.md | 4 +++- plugins/homeassistant/mcp_tools.py | 10 ++++++---- plugins/homeassistant/schedule.py | 2 +- plugins/homeassistant/service.py | 7 +++++-- .../templates/homeassistant/index.html | 7 +++---- plugins/homeassistant/updates.py | 4 +++- tests/integration/test_homeassistant.py | 19 +++++++++++++++---- tests/plugins/homeassistant/test_service.py | 8 +++++--- 8 files changed, 41 insertions(+), 20 deletions(-) diff --git a/docs/plugins/homeassistant.md b/docs/plugins/homeassistant.md index 1cef2cd..5d8c8f9 100644 --- a/docs/plugins/homeassistant.md +++ b/docs/plugins/homeassistant.md @@ -29,6 +29,8 @@ Install asks once, then Steward: One runs at a time, and it carries on if Steward restarts. Firmware and custom integrations are left to Home Assistant, and Supervisor updates itself. +An OS update takes effect only after the machine reboots, and Steward leaves that to you. Steward records the update as done once Home Assistant reports the new version, which it does before the reboot. Home Assistant then raises a repair asking for the reboot, which appears under System problems until you do it. + ## System problems Each read also asks Home Assistant whether the system itself has a problem, and the page and the overview show what it says: @@ -43,7 +45,7 @@ Repairs need an administrator's sign-in. Disk space and add-ons come from the Su ## Scheduled updates -Set a day and time under Scheduled updates on the page. It's off until you do. At that time Steward installs what's waiting, add-ons first, then Core, then the OS, one at a time and each after its own confirmed backup. It starts nothing new more than 4 hours after the slot. The first failure ends the run and the rest wait, and the page, the overview and `update_failed` say so. A Core update restarts Home Assistant and an OS update reboots the machine, with nobody there to watch, and an update can change how automations behave. +Set a day and time under Scheduled updates on the page. It's off until you do. At that time Steward installs what's waiting, add-ons first, then Core, then the OS, one at a time and each after its own confirmed backup. It starts nothing new more than 4 hours after the slot. The first failure ends the run and the rest wait, and the page, the overview and `update_failed` say so. A Core update restarts Home Assistant with nobody there to watch, an update can change how automations behave, and an OS update still needs you to reboot the machine afterwards. ## Metrics diff --git a/plugins/homeassistant/mcp_tools.py b/plugins/homeassistant/mcp_tools.py index 63bd698..2ee45cd 100644 --- a/plugins/homeassistant/mcp_tools.py +++ b/plugins/homeassistant/mcp_tools.py @@ -82,8 +82,9 @@ async def update_home_assistant(entity_id: str) -> dict: entity_id: from get_home_assistant's `installable`. - An update restarts Home Assistant or the add-on, and an OS update reboots the - machine. Refused while another is under way, or if nothing is waiting there. + An update restarts Home Assistant or the add-on. An OS update reads as done + once it's written, and takes effect after a reboot, which Steward leaves to a + person. Refused while another is under way, or if nothing is waiting there. Admin only, as on the page. Recorded in the audit log. """ caller = current_caller() @@ -104,8 +105,9 @@ async def set_home_assistant_schedule(enabled: bool, weekday: int = 5, at_time: At each slot Steward installs add-ons, then Core, then the OS, one at a time, each after a confirmed backup, stopping at the first failure. A Core update - restarts Home Assistant and an OS update reboots the machine, with nobody - watching. The first slot is the next one from now. Admin only, and audited. + restarts Home Assistant with nobody watching. An OS update takes effect after + a reboot, which Steward leaves to a person, and `system.problems` shows it until + then. The first slot is the next one from now. Admin only, and audited. """ try: weekly.parse_at(at_time) diff --git a/plugins/homeassistant/schedule.py b/plugins/homeassistant/schedule.py index 48708e3..517a1b3 100644 --- a/plugins/homeassistant/schedule.py +++ b/plugins/homeassistant/schedule.py @@ -1,7 +1,7 @@ # plugins/homeassistant/schedule.py """Installing what's waiting at a weekly slot: one at a time through the same path as the page's Install, each after its own confirmed backup. Add-ons go first and -the OS last, as it reboots the machine. The first failure ends the slot, leaving the +the OS last, as it waits on a reboot. The first failure ends the slot, leaving the rest for someone to look at. Everything is on the rows, so a restart of Steward or of Home Assistant only delays the next step.""" from __future__ import annotations diff --git a/plugins/homeassistant/service.py b/plugins/homeassistant/service.py index 2afbf2e..08f3020 100644 --- a/plugins/homeassistant/service.py +++ b/plugins/homeassistant/service.py @@ -141,8 +141,10 @@ def read_health(found: dict) -> dict: problems.append({"kind": "supervisor", "severity": "error" if word == "unhealthy" else "warning", "title": f"The Supervisor calls this system {word}: {str(reason).replace('_', ' ')}."}) host = found.get("host") or {} - free, total = host.get("disk_free"), host.get("disk_total") + free, used, total = host.get("disk_free"), host.get("disk_used"), host.get("disk_total") free_pct = round(100 * free / total, 1) if free is not None and total else None + # Used and free don't add up to the total: the system reserves some of the disk. + used_pct = round(100 * used / total, 1) if used is not None and total else None if free_pct is not None and free_pct < LOW_DISK_PCT: problems.append({"kind": "disk", "severity": "error", "title": f"Only {free:g} GB of {total:g} GB free."}) boots = found.get("boots") or {} @@ -151,7 +153,8 @@ def read_health(found: dict) -> dict: problems.append({"kind": "addon", "severity": "error", "title": f"{addon.get('name') or addon.get('slug')} should start on its own and isn't running."}) return {"repairs_read": issues is not None, "supervised": found.get("resolution") is not None, "problems": problems, - "disk_free_gb": free, "disk_total_gb": total, "disk_free_pct": free_pct} + "disk_free_gb": free, "disk_used_gb": used, "disk_total_gb": total, + "disk_free_pct": free_pct, "disk_used_pct": used_pct} def problems_open(health: dict | None) -> list[dict]: diff --git a/plugins/homeassistant/templates/homeassistant/index.html b/plugins/homeassistant/templates/homeassistant/index.html index 6c3e4db..eda8cd6 100644 --- a/plugins/homeassistant/templates/homeassistant/index.html +++ b/plugins/homeassistant/templates/homeassistant/index.html @@ -37,8 +37,7 @@
{{ stat_tile("System problems", problems|length if health.repairs_read else "—", state="degraded" if problems else none) }} - {{ stat_tile("Storage", (health.disk_total_gb ~ " GB") if health.disk_total_gb else "—", - hint=("%.0f%% full"|format(100 - health.disk_free_pct)) if health.disk_free_pct is number else none) }} + {{ stat_tile("Storage", ("%s of %s GB (%.0f%%)"|format(health.disk_used_gb, health.disk_total_gb, health.disk_used_pct)) if health.disk_used_pct is number else "—") }} {{ stat_tile("Updates waiting", installable|length if connection.last_ok_at else "—") }} {{ stat_tile("Last good backup", (backup_at|ago) if backup_at else "—", state="degraded" if backup_tone == 'warn' else none) }} {{ stat_tile("Next backup", (backup_next|ago) if backup_next else "—") }} @@ -78,7 +77,7 @@ {% if admin %} {% if connection.can_install and not under_way and not connection.refused and not u.in_progress %}
{{ csrf_field() }} + data-confirm="Install {{ u.title }} {{ u.latest }}? Steward takes a backup first and won't install without it. {{ 'Home Assistant restarts.' if u.kind == 'core' else ('It takes effect once you reboot the machine, which Steward leaves to you.' if u.kind == 'os' else 'The add-on restarts.') }}">{{ csrf_field() }}
@@ -104,7 +103,7 @@ {% endif %} {% if plan and plan.stopped %}

The last run stopped early. {{ plan.stopped }}

{% endif %} {% if admin %} -

A Core update restarts Home Assistant and an OS update reboots the machine, with nobody there to watch. An update can also change how automations behave.

+

A Core update restarts Home Assistant with nobody there to watch, and an update can change how automations behave. An OS update takes effect only once you reboot the machine: Steward leaves that to you, and shows it under System until it's done.

{{ csrf_field() }}