"""Board model. Supported board types --------------------- olimex_esp32_c6 – Olimex ESP32-C6-EVB (4 relays, 4 digital inputs) Communicates via HTTP REST + webhook callbacks generic_esp32 – Generic ESP32 with custom firmware (configurable I/O) generic_esp8266 – Generic ESP8266 with custom firmware (configurable I/O) """ import json from datetime import datetime from app import db class Board(db.Model): __tablename__ = "boards" id = db.Column(db.Integer, primary_key=True) # Human-readable local name name = db.Column(db.String(128), nullable=False) board_type = db.Column(db.String(32), nullable=False, default="olimex_esp32_c6_evb") host = db.Column(db.String(128), nullable=False) port = db.Column(db.Integer, nullable=False, default=80) # Number of relays / outputs exposed by this board num_relays = db.Column(db.Integer, nullable=False, default=4) # Number of digital inputs num_inputs = db.Column(db.Integer, nullable=False, default=4) # JSON blob: {"relay_1": true, "relay_2": false, ...} relay_states_json = db.Column(db.Text, default="{}") # JSON blob: {"input_1": false, "input_2": true, ...} input_states_json = db.Column(db.Text, default="{}") # Relay and input labels (legacy, kept for backward compat) labels_json = db.Column(db.Text, default="{}") # Entity config: {"relay_1": {"type": "light", "name": "...", "icon": ""}, ...} entities_json = db.Column(db.Text, default="{}") # Whether this board is currently reachable is_online = db.Column(db.Boolean, default=False) last_seen = db.Column(db.DateTime, nullable=True) # Firmware version string reported by the board firmware_version = db.Column(db.String(64), nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) # Extra driver-specific config (JSON) – e.g. eWeLink credentials/token config_json = db.Column(db.Text, default="{}") # ── relationships ──────────────────────────────────────────────── workflows_trigger = db.relationship( "Workflow", foreign_keys="Workflow.trigger_board_id", back_populates="trigger_board", cascade="all, delete-orphan" ) workflows_target = db.relationship( "Workflow", foreign_keys="Workflow.action_board_id", back_populates="action_board" ) sonoff_devices = db.relationship( "SonoffDevice", back_populates="board", cascade="all, delete-orphan", lazy="dynamic" ) # ── helpers ────────────────────────────────────────────────────── @property def relay_states(self) -> dict: try: return json.loads(self.relay_states_json or "{}") except (ValueError, TypeError): return {} @relay_states.setter def relay_states(self, value: dict): self.relay_states_json = json.dumps(value) @property def input_states(self) -> dict: try: return json.loads(self.input_states_json or "{}") except (ValueError, TypeError): return {} @input_states.setter def input_states(self, value: dict): self.input_states_json = json.dumps(value) @property def labels(self) -> dict: try: return json.loads(self.labels_json or "{}") except (ValueError, TypeError): return {} @labels.setter def labels(self, value: dict): self.labels_json = json.dumps(value) @property def entities(self) -> dict: try: return json.loads(self.entities_json or "{}") except (ValueError, TypeError): return {} @entities.setter def entities(self, value: dict): self.entities_json = json.dumps(value) def get_relay_entity(self, n: int) -> dict: from app.models.entity_types import RELAY_ENTITY_TYPES cfg = self.entities.get(f"relay_{n}", {}) etype = cfg.get("type", "switch") tdef = RELAY_ENTITY_TYPES.get(etype, RELAY_ENTITY_TYPES["switch"]) # name: entities > legacy labels > type default name = cfg.get("name", "").strip() or self.labels.get(f"relay_{n}", "") or f"{tdef['label']} {n}" icon = cfg.get("icon", "").strip() or tdef["icon"] return { "name": name, "icon": icon, "type": etype, "on_color": tdef["on"][0], "on_label": tdef["on"][1], "off_color": tdef["off"][0], "off_label": tdef["off"][1], } def get_input_entity(self, n: int) -> dict: from app.models.entity_types import INPUT_ENTITY_TYPES cfg = self.entities.get(f"input_{n}", {}) etype = cfg.get("type", "generic") tdef = INPUT_ENTITY_TYPES.get(etype, INPUT_ENTITY_TYPES["generic"]) name = cfg.get("name", "").strip() or self.labels.get(f"input_{n}", "") or f"{tdef['label']} {n}" icon = cfg.get("icon", "").strip() or tdef["icon"] return { "name": name, "icon": icon, "type": etype, "active_color": tdef["active"][0], "active_label": tdef["active"][1], "idle_color": tdef["idle"][0], "idle_label": tdef["idle"][1], } @property def config(self) -> dict: try: return json.loads(self.config_json or "{}") except (ValueError, TypeError): return {} @config.setter def config(self, value: dict): self.config_json = json.dumps(value) @property def is_sonoff_gateway(self) -> bool: return self.board_type == "sonoff_ewelink" def get_relay_label(self, relay_num: int) -> str: return self.get_relay_entity(relay_num)["name"] def get_input_label(self, input_num: int) -> str: return self.get_input_entity(input_num)["name"] @property def base_url(self) -> str: return f"http://{self.host}:{self.port}" def __repr__(self) -> str: return f""