Initial commit: Location Management Flask app

This commit is contained in:
ske087
2026-02-26 19:24:17 +02:00
commit 7a22575dab
52 changed files with 3481 additions and 0 deletions

144
app/models/board.py Normal file
View 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}>"