From b4db41a4009e14ce599e377eb0836540985e756a Mon Sep 17 00:00:00 2001 From: ske087 Date: Fri, 27 Feb 2026 16:20:10 +0200 Subject: [PATCH] Fix offline board flooding: fail-fast probe in hardware driver poll() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/drivers/generic_esp32/driver.py | 26 +++++++++++++++----- app/drivers/generic_esp8266/driver.py | 26 +++++++++++++++----- app/drivers/olimex_esp32_c6_evb/driver.py | 30 ++++++++++++++++++----- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/app/drivers/generic_esp32/driver.py b/app/drivers/generic_esp32/driver.py index 6dd841b..c6bbceb 100644 --- a/app/drivers/generic_esp32/driver.py +++ b/app/drivers/generic_esp32/driver.py @@ -70,21 +70,35 @@ class GenericESP32Driver(BoardDriver): return bool(data["state"]) if data is not None else None def poll(self, board: "Board") -> dict: - relay_states, input_states, online = {}, {}, False + _offline = {"relay_states": {}, "input_states": {}, "is_online": False} + relay_states, input_states = {}, {} - for n in range(1, board.num_relays + 1): + # 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)) - online = True - for n in range(1, board.num_inputs + 1): + 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)) - online = True - return {"relay_states": relay_states, "input_states": input_states, "is_online": online} + 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 diff --git a/app/drivers/generic_esp8266/driver.py b/app/drivers/generic_esp8266/driver.py index c67e5a0..ff0ca91 100644 --- a/app/drivers/generic_esp8266/driver.py +++ b/app/drivers/generic_esp8266/driver.py @@ -60,21 +60,35 @@ class GenericESP8266Driver(BoardDriver): return bool(data["state"]) if data is not None else None def poll(self, board: "Board") -> dict: - relay_states, input_states, online = {}, {}, False + _offline = {"relay_states": {}, "input_states": {}, "is_online": False} + relay_states, input_states = {}, {} - for n in range(1, board.num_relays + 1): + # 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)) - online = True - for n in range(1, board.num_inputs + 1): + 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)) - online = True - return {"relay_states": relay_states, "input_states": input_states, "is_online": online} + 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 diff --git a/app/drivers/olimex_esp32_c6_evb/driver.py b/app/drivers/olimex_esp32_c6_evb/driver.py index c0ead3b..8b58e0e 100644 --- a/app/drivers/olimex_esp32_c6_evb/driver.py +++ b/app/drivers/olimex_esp32_c6_evb/driver.py @@ -83,26 +83,44 @@ class OlimexESP32C6EVBDriver(BoardDriver): # ── poll ────────────────────────────────────────────────────────────────── def poll(self, board: "Board") -> dict: + _offline = {"relay_states": {}, "input_states": {}, "is_online": False} + + # ── Connectivity probe ───────────────────────────────────────────── + # Try the very first endpoint. If the board doesn't reply we know + # it's offline and skip all remaining requests (saving N × 3 s worth + # of connect timeouts on every recheck cycle). relay_states: dict = {} input_states: dict = {} - online = False - for n in range(1, board.num_relays + 1): + 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 + + # Board is reachable — collect remaining endpoints + 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)) - online = True - for n in range(1, board.num_inputs + 1): + 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", True)) - online = True return { "relay_states": relay_states, "input_states": input_states, - "is_online": online, + "is_online": True, } # ── webhook registration ──────────────────────────────────────────────────