Files
location_managemet/app/drivers/generic_esp32/driver.py
ske087 b4db41a400 Fix offline board flooding: fail-fast probe in hardware driver poll()
All three hardware drivers (Olimex, generic_esp32, generic_esp8266) now
probe with the first request and bail immediately on failure.

Before: offline board with 4 relays + 3 inputs = 7 requests × 3 s timeout
        = up to 21 s of blocking + 7 log lines per recheck cycle.
After:  exactly 1 request × 3 s timeout regardless of I/O count.
2026-02-27 16:20:10 +02:00

105 lines
3.7 KiB
Python

"""Generic ESP32 board driver (custom firmware).
This is a template driver for custom ESP32 firmware.
Copy this folder, rename it, and implement the HTTP endpoints
to match your own firmware's API.
Expected firmware endpoints (same shape as Olimex by default):
POST /relay/on?relay=<n>
POST /relay/off?relay=<n>
POST /relay/toggle?relay=<n>
GET /relay/status?relay=<n> → {"state": <bool>}
GET /input/status?input=<n> → {"state": <bool>}
POST /register?callback_url=<url>
"""
from __future__ import annotations
import logging
import requests
from typing import TYPE_CHECKING
from app.drivers.base import BoardDriver
if TYPE_CHECKING:
from app.models.board import Board
logger = logging.getLogger(__name__)
_TIMEOUT = 3
def _get(url):
try:
r = requests.get(url, timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
except Exception as exc:
logger.debug("GET %s%s", url, exc)
return None
def _post(url):
try:
r = requests.post(url, timeout=_TIMEOUT)
r.raise_for_status()
return r.json()
except Exception as exc:
logger.debug("POST %s%s", url, exc)
return None
class GenericESP32Driver(BoardDriver):
"""Generic ESP32 driver — uses the same REST conventions as the Olimex board.
Customise the endpoint paths below to match your firmware."""
DRIVER_ID = "generic_esp32"
DISPLAY_NAME = "Generic ESP32"
DESCRIPTION = "Custom ESP32 firmware · same REST API shape as Olimex"
DEFAULT_NUM_RELAYS = 4
DEFAULT_NUM_INPUTS = 4
def get_relay_status(self, board: "Board", relay_num: int) -> bool | None:
data = _get(f"{board.base_url}/relay/status?relay={relay_num}")
return bool(data["state"]) if data is not None else None
def set_relay(self, board: "Board", relay_num: int, state: bool) -> bool:
action = "on" if state else "off"
return _post(f"{board.base_url}/relay/{action}?relay={relay_num}") is not None
def toggle_relay(self, board: "Board", relay_num: int) -> bool | None:
data = _post(f"{board.base_url}/relay/toggle?relay={relay_num}")
return bool(data["state"]) if data is not None else None
def poll(self, board: "Board") -> dict:
_offline = {"relay_states": {}, "input_states": {}, "is_online": False}
relay_states, input_states = {}, {}
# Fail-fast probe — bail immediately if board is unreachable
if board.num_relays > 0:
probe = _get(f"{board.base_url}/relay/status?relay=1")
if probe is None:
return _offline
relay_states["relay_1"] = bool(probe.get("state", False))
elif board.num_inputs > 0:
probe = _get(f"{board.base_url}/input/status?input=1")
if probe is None:
return _offline
input_states["input_1"] = bool(probe.get("state", False))
else:
return _offline
for n in range(2, board.num_relays + 1):
data = _get(f"{board.base_url}/relay/status?relay={n}")
if data is not None:
relay_states[f"relay_{n}"] = bool(data.get("state", False))
input_start = 2 if (board.num_relays == 0 and board.num_inputs > 0) else 1
for n in range(input_start, board.num_inputs + 1):
data = _get(f"{board.base_url}/input/status?input={n}")
if data is not None:
input_states[f"input_{n}"] = bool(data.get("state", False))
return {"relay_states": relay_states, "input_states": input_states, "is_online": True}
def register_webhook(self, board: "Board", callback_url: str) -> bool:
return _post(f"{board.base_url}/register?callback_url={callback_url}") is not None