Files
home-automation/app/models/modbus.py
T

115 lines
4.3 KiB
Python
Raw Permalink Normal View History

"""SQLAlchemy models for Modbus device management and telemetry.
Two tables:
- modbus_device: deployment/configurable metadata for each polled device.
- modbus_reading: generic telemetry rows (one per device per poll cycle).
"""
from __future__ import annotations
import uuid as _uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from app.db import Base
def _uuid4_str() -> str:
return str(_uuid.uuid4())
class ModbusDevice(Base):
"""Deployment-layer record for a Modbus slave device reachable via a TCP gateway.
Protocol knowledge (register map, decoding rules) lives in the YAML profile
referenced by ``profile``. Per-device variables (network address, slave ID,
display name, poll rate) live here.
"""
__tablename__ = "modbus_device"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# Stable internal identity — used as the API path key and HA Discovery unique_id anchor.
uuid: Mapped[str] = mapped_column(
String(36), unique=True, nullable=False, default=_uuid4_str
)
# Human-readable label (may be changed; changing it triggers re-publish of HA Discovery).
friendly_name: Mapped[str] = mapped_column(String(255), nullable=False)
# Transport protocol — only "tcp" is supported for now.
transport: Mapped[str] = mapped_column(String(16), nullable=False, default="tcp")
# TCP gateway address.
host: Mapped[str] = mapped_column(String(255), nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False, default=502)
# Modbus slave address (set on the device panel; different meters on the same gateway
# must have distinct unit_ids).
unit_id: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
# Which YAML profile to use for register mapping and decoding (e.g. "sdm120").
profile: Mapped[str] = mapped_column(String(64), nullable=False)
# Polling interval in seconds.
poll_interval_s: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
# Whether this device is included in the periodic poll sweep.
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
# Poll-status fields (updated by the poll service after each attempt).
last_poll_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
last_poll_ok: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
# Audit timestamps.
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
# Relationship to readings (back-reference; not loaded eagerly).
readings: Mapped[list["ModbusReading"]] = relationship(
back_populates="device", cascade="save-update, merge"
)
class ModbusReading(Base):
"""Generic telemetry row: one device, one poll instant, all decoded metrics as JSON.
``payload`` contains the full dict of engineering values produced by the YAML profile
decoder, e.g. ``{"voltage": 230.2, "current": 1.3, ...}``. The profile is the key
to interpreting which keys exist and what units they carry.
"""
__tablename__ = "modbus_reading"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# FK to the device that produced this reading.
# ON DELETE RESTRICT: prevents accidental deletion of a device that has historical data.
device_id: Mapped[int] = mapped_column(
ForeignKey("modbus_device.id", ondelete="RESTRICT"), nullable=False
)
# The UTC timestamp of when the sample was taken — real indexed column, NOT embedded in payload.
recorded_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, index=True
)
# Profile-decoded engineering values as a JSON object.
payload: Mapped[dict] = mapped_column(JSON, nullable=False)
device: Mapped["ModbusDevice"] = relationship(back_populates="readings")
# Composite index for efficient time-range queries scoped to a single device.
Index("ix_modbus_reading_device_recorded", ModbusReading.device_id, ModbusReading.recorded_at)