# 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)