Initial commit: Location Management Flask app
This commit is contained in:
144
app/models/board.py
Normal file
144
app/models/board.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""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)
|
||||
|
||||
# ── 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"
|
||||
)
|
||||
|
||||
# ── 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],
|
||||
}
|
||||
|
||||
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"<Board {self.name} ({self.board_type}) @ {self.host}:{self.port}>"
|
||||
Reference in New Issue
Block a user