fix(homeassistant): storage reads used of total with its percentage, and an OS update is said to wait on a reboot (#6753, #6754)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m36s
CI / integration (push) Successful in 1m59s
CI / publish (push) Successful in 19s

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 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 18:51:28 -05:00
co-authored by Claude Opus 5.5
parent 5ffececae5
commit 4c57735dd9
8 changed files with 41 additions and 20 deletions
+6 -4
View File
@@ -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)
+1 -1
View File
@@ -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
+5 -2
View File
@@ -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]:
@@ -37,8 +37,7 @@
<div class="stat-strip">
{{ 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 %}<td style="text-align:right;">
{% if connection.can_install and not under_way and not connection.refused and not u.in_progress %}
<form method="post" action="/plugins/homeassistant/install"
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 ('The machine reboots.' if u.kind == 'os' else 'The add-on restarts.') }}">{{ 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() }}
<input type="hidden" name="entity_id" value="{{ u.entity_id }}">
<button class="btn btn-sm" type="submit">Install</button>
</form>
@@ -104,7 +103,7 @@
{% endif %}
{% if plan and plan.stopped %}<p class="tone-warn">The last run stopped early. {{ plan.stopped }}</p>{% endif %}
{% if admin %}
<p class="tone-warn">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.</p>
<p class="tone-warn">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.</p>
<form method="post" action="/plugins/homeassistant/schedule" style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:flex-end;">{{ csrf_field() }}
<label style="display:flex;gap:0.5rem;align-items:center;cursor:pointer;margin-bottom:0.4rem;">
<input type="checkbox" name="enabled" value="on"{% if plan and plan.enabled %} checked{% endif %}> Install on a schedule
+3 -1
View File
@@ -19,7 +19,9 @@ logger = logging.getLogger(__name__)
UNDER_WAY = ("backing_up", "installing")
BACKUP_DEADLINE_S = 30 * 60
# An OS update reboots the machine, and a Pi is slow to come back.
# An OS update downloads and writes a whole system image, which is slow on a Pi. Home
# Assistant reports the new version as soon as that's written, before the reboot that
# makes it run, and raises a repair asking for that reboot.
INSTALL_DEADLINE_S = {"addon": 20 * 60, "core": 30 * 60, "os": 45 * 60}