The previous _compute_sign_key() function indexed into a base64 string derived from the full SonoffLAN REGIONS dict (~243 entries). Our partial dict only produced a 7876-char a string but needed index 7872+, so the function must use the full dict. Solution: pre-compute the key once from the full dict and hardcode the resulting 32-byte ASCII key. This is deterministic — the SonoffLAN algorithm always produces the same output regardless of when it runs. The sonoff_ewelink driver now loads cleanly alongside all other drivers.
167 lines
6.2 KiB
Python
167 lines
6.2 KiB
Python
"""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"<Board {self.name} ({self.board_type}) @ {self.host}:{self.port}>"
|