Six silent failures found by installing this on a live system
Migrated the reference site off the YAML packages and onto the add-on. Every bug below presented identically: the add-on starts, logs "started", serves its UI, and cannot do its job. - run.sh needs #!/usr/bin/with-contenv sh. s6-overlay sanitises the environment for services, so a plain shebang means SUPERVISOR_TOKEN is absent and every Core API call is 401 - while homeassistant_api: true makes permissions look granted. Startup now prints the token length and probes the API. - Supervisor keys the image by config.yaml `version`, so rebuilding without a bump reuses the old image. Two fixes appeared not to work because of it. - Alpine is musl and has no aiohttp wheel on PyPI; deps now come from apk so nothing compiles on a client's Pi. - Alpine ships paho-mqtt 1.x, which has no CallbackAPIVersion. That raised at construction and took the control loop down with it - so MQTT setup is now wrapped too. Observability must never be able to stop the controller. - MQTT discovery is published from on_connect: paho silently drops QoS-0 publishes issued before the CONNACK, so the previous code announced nothing while logging "MQTT connected". - Repeated failures now log once a minute. Six warnings a second rolled the log buffer and destroyed the startup diagnostics needed to find the 401. - auto_start could never fire, because the store's defaults always supplied auto: False for the fallback to find. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016NckgXecasQb2eSsPYNSW6
This commit is contained in:
@@ -7,6 +7,7 @@ leak at a client site. That is one of the main reasons this is an add-on.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -21,9 +22,23 @@ class HomeAssistant:
|
||||
def __init__(self, session: aiohttp.ClientSession, token: str | None = None):
|
||||
self.session = session
|
||||
self.token = token or os.environ.get("SUPERVISOR_TOKEN", "")
|
||||
self._last_moan: dict[str, float] = {}
|
||||
if not self.token:
|
||||
_LOG.error("SUPERVISOR_TOKEN missing - is this running as an add-on?")
|
||||
|
||||
def _moan(self, key: str, msg: str, *args) -> None:
|
||||
"""Log a recurring failure at most once a minute.
|
||||
|
||||
⚠️ The control loop retries every second, so an unthrottled warning here
|
||||
writes six lines a second forever - which rolls the add-on's log buffer
|
||||
and destroys exactly the startup diagnostics an installer needs. A fault
|
||||
that repeats is not more informative for being repeated.
|
||||
"""
|
||||
now = time.monotonic()
|
||||
if now - self._last_moan.get(key, -999) >= 60:
|
||||
self._last_moan[key] = now
|
||||
_LOG.warning(msg, *args)
|
||||
|
||||
@property
|
||||
def _headers(self) -> dict:
|
||||
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
|
||||
@@ -48,7 +63,7 @@ class HomeAssistant:
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOG.warning("read %s failed: %s", entity_id, err)
|
||||
self._moan(f"read:{entity_id}", "read %s failed: %s", entity_id, err)
|
||||
return None
|
||||
|
||||
async def number(self, entity_id: str, invert: bool = False) -> float | None:
|
||||
@@ -62,7 +77,7 @@ class HomeAssistant:
|
||||
try:
|
||||
value = float(raw)
|
||||
except ValueError:
|
||||
_LOG.warning("%s is not numeric: %r", entity_id, raw)
|
||||
self._moan(f"nan:{entity_id}", "%s is not numeric: %r", entity_id, raw)
|
||||
return None
|
||||
return -value if invert else value
|
||||
|
||||
@@ -97,12 +112,14 @@ class HomeAssistant:
|
||||
# value is rejected outright, not clamped. Always log it:
|
||||
# silently dropped commands are how a controller ends up
|
||||
# believing something the hardware never did.
|
||||
_LOG.error("service %s.%s rejected (%s): %s",
|
||||
self._moan(f"svc:{domain}.{service}",
|
||||
"service %s.%s rejected (%s): %s",
|
||||
domain, service, resp.status, body[:200])
|
||||
return False
|
||||
return True
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
_LOG.warning("service %s.%s failed: %s", domain, service, err)
|
||||
self._moan(f"svcerr:{domain}.{service}", "service %s.%s failed: %s",
|
||||
domain, service, err)
|
||||
return False
|
||||
|
||||
async def set_number(self, entity_id: str, value: float) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user