location_ha_sensor.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. from datetime import datetime
  2. from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, String, func
  3. from sqlalchemy.orm import Mapped, mapped_column, relationship
  4. from backend.app.core.database import Base
  5. # Width of the last_state column. The poller truncates what it persists to
  6. # this, because a numeric entity can start reporting free text (an enum, an
  7. # error string) longer than the column — SQLite stores it anyway, but
  8. # PostgreSQL rejects the row and takes the whole poll batch's commit with it.
  9. LAST_STATE_MAX_LENGTH = 64
  10. class LocationHASensor(Base):
  11. """A read-only Home Assistant entity bound to a storage location (#2824).
  12. Mirrors ``PrinterHASensor`` for dryboxes, bins and shelves instead of
  13. printers — same read-only binding, alert rule and notification, but no
  14. print-blocking: holding a print queue doesn't mean anything for a
  15. storage bin.
  16. """
  17. __tablename__ = "location_ha_sensors"
  18. # The API rejects a duplicate (location, entity) binding, but that check is
  19. # read-then-insert — two concurrent creates can both pass it. This index is
  20. # the backstop that turns the loser into an IntegrityError instead of a
  21. # second row silently shadowing the first. create_all() only covers fresh
  22. # installs; upgraded databases get it from
  23. # _migrate_location_ha_sensor_unique_binding in core/database.py, which
  24. # must create the same index under the same name.
  25. __table_args__ = (Index("uq_location_ha_sensors_location_entity", "location_id", "entity_id", unique=True),)
  26. id: Mapped[int] = mapped_column(primary_key=True)
  27. location_id: Mapped[int] = mapped_column(ForeignKey("locations.id", ondelete="CASCADE"), index=True)
  28. name: Mapped[str] = mapped_column(String(100))
  29. entity_id: Mapped[str] = mapped_column(String(255))
  30. # "binary" for binary_sensor.*, "numeric" for sensor.*. Decides how the
  31. # state is rendered and which alert fields apply.
  32. kind: Mapped[str] = mapped_column(String(16), default="binary")
  33. # HA's own device_class, snapshotted when the entity is bound. Drives the
  34. # category a sensor is treated as (temperature/humidity/battery) and the
  35. # unit shown next to the value.
  36. device_class: Mapped[str | None] = mapped_column(String(32), nullable=True)
  37. # Numeric only: "°C", "%", ... shown next to the value.
  38. unit: Mapped[str | None] = mapped_column(String(16), nullable=True)
  39. # What counts as needing attention. One notion, two consumers: the
  40. # colorized value on the card/table and the notification. Binary sensors
  41. # use alert_state ("on"/"off"/None), numeric ones the thresholds. All
  42. # None means "just show the value".
  43. alert_state: Mapped[str | None] = mapped_column(String(8), nullable=True)
  44. alert_above: Mapped[float | None] = mapped_column(Float, nullable=True)
  45. alert_below: Mapped[float | None] = mapped_column(Float, nullable=True)
  46. notify_on_alert: Mapped[bool] = mapped_column(Boolean, default=False, server_default="0")
  47. show_on_card: Mapped[bool] = mapped_column(Boolean, default=True, server_default="1")
  48. sort_order: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
  49. # Last poll result. Persisted so a restart doesn't blank the card until the
  50. # first poll lands, and so notifications only fire on a real transition.
  51. last_state: Mapped[str | None] = mapped_column(String(LAST_STATE_MAX_LENGTH), nullable=True)
  52. last_changed: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  53. last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  54. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  55. updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
  56. location: Mapped["Location"] = relationship(back_populates="ha_sensors")
  57. from backend.app.models.location import Location # noqa: E402