Files
location_managemet/app/models/device.py
2026-03-30 15:49:26 +03:00

106 lines
4.8 KiB
Python

"""User-defined device abstraction.
A Device is a human-friendly alias for a board relay (controllable output) or
a board digital input (sensor/trigger) that the user wants to expose as a named
entity — e.g. "Outdoor Light 1 (Courtyard)" mapped to Board "Garage" / Relay 4.
Devices provide an intermediate, named layer so they can later be placed on
Layout pages as interactive widgets without the UI needing raw board/relay IDs.
"""
from datetime import datetime
from app import db
class Device(db.Model):
__tablename__ = "devices"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False)
description = db.Column(db.String(256), nullable=True)
area = db.Column(db.String(64), nullable=True) # e.g. "Courtyard", "Living Room"
# Device category — drives default icon and state labels
device_class = db.Column(db.String(32), nullable=False, default="switch")
# Bootstrap-Icons class override; None → use type default
icon = db.Column(db.String(64), nullable=True)
# ── Source entity on a board ─────────────────────────────────────────────
board_id = db.Column(
db.Integer, db.ForeignKey("boards.id", ondelete="SET NULL"), nullable=True
)
entity_type = db.Column(db.String(16), nullable=True) # "relay" | "input"
entity_num = db.Column(db.Integer, nullable=True) # 1-based relay/input index
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# ── relationships ────────────────────────────────────────────────────────
board = db.relationship("Board", back_populates="devices")
# ── helpers ──────────────────────────────────────────────────────────────
@property
def is_controllable(self) -> bool:
"""True when the source entity is a relay (can be toggled ON/OFF)."""
return self.entity_type == "relay"
@property
def effective_icon(self) -> str:
"""Icon class to use in the UI (custom override or type default)."""
from app.models.entity_types import RELAY_ENTITY_TYPES, INPUT_ENTITY_TYPES
if self.icon:
return self.icon
if self.entity_type == "relay":
return RELAY_ENTITY_TYPES.get(
self.device_class, RELAY_ENTITY_TYPES["switch"]
)["icon"]
if self.entity_type == "input":
return INPUT_ENTITY_TYPES.get(
self.device_class, INPUT_ENTITY_TYPES["generic"]
)["icon"]
return "bi-cpu"
@property
def current_state(self):
"""Returns the current boolean state or None if unavailable."""
if not self.board or not self.entity_type or not self.entity_num:
return None
if self.entity_type == "relay":
return self.board.relay_states.get(f"relay_{self.entity_num}", False)
if self.entity_type == "input":
raw = self.board.input_states.get(f"input_{self.entity_num}", True)
return not raw # NC contact: raw True = resting → False = idle
return None
@property
def state_label(self) -> str:
"""Human-readable state string."""
from app.models.entity_types import RELAY_ENTITY_TYPES, INPUT_ENTITY_TYPES
state = self.current_state
if state is None:
return "Unknown"
if self.entity_type == "relay":
tdef = RELAY_ENTITY_TYPES.get(self.device_class, RELAY_ENTITY_TYPES["switch"])
return tdef["on"][1] if state else tdef["off"][1]
if self.entity_type == "input":
tdef = INPUT_ENTITY_TYPES.get(self.device_class, INPUT_ENTITY_TYPES["generic"])
return tdef["active"][1] if state else tdef["idle"][1]
return "Unknown"
@property
def state_color(self) -> str:
"""Bootstrap color name for current state badge."""
from app.models.entity_types import RELAY_ENTITY_TYPES, INPUT_ENTITY_TYPES
state = self.current_state
if state is None:
return "secondary"
if self.entity_type == "relay":
tdef = RELAY_ENTITY_TYPES.get(self.device_class, RELAY_ENTITY_TYPES["switch"])
return tdef["on"][0] if state else tdef["off"][0]
if self.entity_type == "input":
tdef = INPUT_ENTITY_TYPES.get(self.device_class, INPUT_ENTITY_TYPES["generic"])
return tdef["active"][0] if state else tdef["idle"][0]
return "secondary"
def __repr__(self):
return f"<Device {self.name!r} id={self.id}>"