"""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""