Replaces the read-only token and the webhook automation, which every install would have had to paste into its own Home Assistant. Connect now goes to Home Assistant's own sign-in page, and Steward keeps the refresh token that comes back, encrypted, and calls backup.create_automatic and update.install directly. The connection page warns that the stored credential has the same access as the user who signed in, and recommends a service account so Steward's actions are easy to tell apart in Home Assistant's logs. A user who isn't an administrator gets the status page and no Install. Those actions answer only when finished, so a request cut off once sent counts as sent and the outcome is read from the entities. Removing the connection, or signing in again, ends the old sign-in on Home Assistant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
4.0 KiB
Python
71 lines
4.0 KiB
Python
# 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.
|
|
|
|
`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` 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)
|
|
|
|
|
|
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)
|