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:
@@ -0,0 +1,68 @@
|
||||
# plugins/homeassistant/models.py
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import JSON, Boolean, DateTime, Index, 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.
|
||||
|
||||
The token is a read-only user's and the webhook id can only reach the
|
||||
maintenance automation, so nothing stored here can act on the house. Both
|
||||
are encrypted. The last_* columns and `updates` and `backup` are the newest
|
||||
poll's outcome, which the pages read. `refused` stops all polling until
|
||||
someone saves the connection 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)
|
||||
token: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
webhook_id: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
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)
|
||||
automation: Mapped[str | None] = mapped_column(String(20), 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)
|
||||
triggered_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)
|
||||
Reference in New Issue
Block a user