The connection's job is keeping Home Assistant running. Each poll now reads open
repairs, Supervisor health, disk, recovery mode and add-ons that should be running
over its websocket, for the page, the overview and the problems_open, disk_free_pct
and update_failed metrics. That needs a websocket client, so websockets joins the
dependencies.
A weekly slot, set on the page or through MCP and off until then, installs add-ons,
then Core, then the OS, each after its own confirmed backup, ending at the first
failure. The slot arithmetic moves from the host agent's cadence to steward.core.weekly
so both share it.
The device scrape goes: the page's Metrics section and a target's token_from from
4c9ed02 are removed, and a migration deletes targets using it, as they'd otherwise ask
with no token. A refused token still stops a scrape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
5.2 KiB
Python
90 lines
5.2 KiB
Python
# plugins/homeassistant/models.py
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import JSON, Boolean, DateTime, Index, Integer, String, Text, text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from steward.core.ids import time_ordered_id
|
|
from steward.models.base import Base
|
|
|
|
|
|
class HomeAssistantConnection(Base):
|
|
"""The Home Assistant instance Steward looks after; at most one row.
|
|
|
|
`refresh_token` is what Home Assistant's sign-in gave Steward, encrypted. It
|
|
has the same access as the user who signed in, whatever that is: Home
|
|
Assistant has no narrower permission. `can_install` is whether that user was
|
|
an administrator, which backups need. The last_* columns and `updates` and
|
|
`backup` and `health` are the newest poll's outcome, which the pages read. `refused` stops
|
|
all polling until someone signs in again or tries again: Home Assistant bans
|
|
an address after a few refused requests.
|
|
"""
|
|
__tablename__ = "homeassistant_connection"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id)
|
|
url: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
client_id: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
refresh_token: Mapped[str] = mapped_column(Text, nullable=False)
|
|
can_install: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
signed_in_by: Mapped[str] = mapped_column(String(100), nullable=False, default="")
|
|
signed_in_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
verify_tls: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
refused: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
last_polled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
last_ok_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
updates: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
|
|
backup: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|
|
health: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|
|
|
|
|
|
class HomeAssistantSchedule(Base):
|
|
"""The weekly slot for installing what's waiting; at most one row, none until an
|
|
admin sets it. `last_slot_at` is the slot that last fired, and `stopped` says why
|
|
that slot ended early. Times are read through steward.core.weekly."""
|
|
__tablename__ = "homeassistant_schedule"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
weekday: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
at_time: Mapped[str] = mapped_column(String(5), nullable=False)
|
|
time_zone: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
window_minutes: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
last_slot_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
stopped: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
|
class HomeAssistantUpdate(Base):
|
|
"""One update Steward was asked to install, from the click to its end. It
|
|
moves through backing_up and installing to done or failed, and its row
|
|
carries everything needed to carry on after Steward restarts. The *_before
|
|
columns are Home Assistant's own readings at the start, so "a newer backup"
|
|
is judged on its clock alone. One can be under way at a time.
|
|
"""
|
|
__tablename__ = "homeassistant_updates"
|
|
__table_args__ = (
|
|
Index("uq_homeassistant_update_under_way", text("(true)"), unique=True,
|
|
postgresql_where=text("state IN ('backing_up', 'installing')")),
|
|
)
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=time_ordered_id)
|
|
entity_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
kind: Mapped[str] = mapped_column(String(10), nullable=False)
|
|
from_version: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
to_version: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
state: Mapped[str] = mapped_column(String(20), nullable=False, default="backing_up")
|
|
message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
started_by: Mapped[str] = mapped_column(String(100), nullable=False, default="")
|
|
started_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
backup_before: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
backup_event_before: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
backup_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
install_sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|