Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7a2f8c78a | |||
| faf4099756 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.15 - 2026-06-16
|
||||
- Add-on-Start ist robust gegen Home-Assistant-Core-502 beim Systemboot:
|
||||
API und WebSocket-Listener starten trotzdem, Reconciliation/Training werden
|
||||
im Hintergrund mit Retry nachgeholt.
|
||||
- Periodische Reconciliation und Fallback-Auswertung beenden den Dienst nicht
|
||||
mehr bei temporären HA-Fehlern.
|
||||
- Add-on-Watchdog prüft `/health`, damit Supervisor den Dienst nach Absturz
|
||||
wieder starten kann.
|
||||
|
||||
## 0.7.14 - 2026-06-16
|
||||
- Onboarding-Vorschläge laden im Dashboard nachgelagert, damit Status,
|
||||
Aktor-Auswahl und bestehende Geräte nicht auf Automation-Discovery warten.
|
||||
|
||||
## 0.7.13 - 2026-06-16
|
||||
- Diagnose-/Schutzsensoren wie Überhitzung und Überlast werden nicht mehr nur
|
||||
wegen gleicher Strom-/Monitoring-Bereiche automatisch als Lichtkontext
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: SillyHome Next
|
||||
version: "0.7.13"
|
||||
version: "0.7.15"
|
||||
slug: sillyhome_next
|
||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||
@@ -7,6 +7,7 @@ arch:
|
||||
- amd64
|
||||
startup: application
|
||||
boot: auto
|
||||
watchdog: http://[HOST]:[PORT:8000]/health
|
||||
init: false
|
||||
ingress: true
|
||||
ingress_port: 8000
|
||||
|
||||
55
app/main.py
55
app/main.py
@@ -43,6 +43,7 @@ class _WsStatus:
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
settings = app.state.settings
|
||||
client: HaClient | None = None
|
||||
startup_task: asyncio.Task[None] | None = None
|
||||
reconcile_task: asyncio.Task[None] | None = None
|
||||
event_listener_task: asyncio.Task[None] | None = None
|
||||
fallback_task: asyncio.Task[None] | None = None
|
||||
@@ -74,15 +75,17 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
settings=settings,
|
||||
)
|
||||
app.state.ws_status = _WsStatus()
|
||||
await asyncio.to_thread(app.state.actuator_service.reconcile_all, "startup")
|
||||
await asyncio.to_thread(app.state.behavior_engine.train_all)
|
||||
await asyncio.to_thread(app.state.behavior_engine.evaluate_all)
|
||||
startup_task = asyncio.create_task(_startup_reconciliation(app))
|
||||
reconcile_task = asyncio.create_task(_periodic_reconciliation(app))
|
||||
event_listener_task = asyncio.create_task(_ha_event_listener(app, client))
|
||||
fallback_task = asyncio.create_task(_fallback_prediction(app))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if startup_task is not None:
|
||||
startup_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await startup_task
|
||||
if reconcile_task is not None:
|
||||
reconcile_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
@@ -102,7 +105,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
app = FastAPI(
|
||||
title="SillyHome Next API",
|
||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||
version="0.7.13",
|
||||
version="0.7.15",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
app.state.settings = load_settings()
|
||||
@@ -147,10 +150,39 @@ async def _periodic_reconciliation(app: FastAPI) -> None:
|
||||
service = getattr(app.state, "actuator_service", None)
|
||||
if not isinstance(service, ActuatorReconciliationService):
|
||||
continue
|
||||
await asyncio.to_thread(service.reconcile_all, "scheduled")
|
||||
try:
|
||||
await asyncio.to_thread(service.reconcile_all, "scheduled")
|
||||
engine = getattr(app.state, "behavior_engine", None)
|
||||
if isinstance(engine, BehaviorEngine):
|
||||
await asyncio.to_thread(engine.train_all)
|
||||
except Exception:
|
||||
logger.exception("Geplante Reconciliation fehlgeschlagen; nächster Lauf versucht es erneut.")
|
||||
|
||||
|
||||
async def _startup_reconciliation(app: FastAPI) -> None:
|
||||
delay_seconds = 5
|
||||
while True:
|
||||
service = getattr(app.state, "actuator_service", None)
|
||||
engine = getattr(app.state, "behavior_engine", None)
|
||||
if isinstance(engine, BehaviorEngine):
|
||||
if not isinstance(service, ActuatorReconciliationService) or not isinstance(
|
||||
engine,
|
||||
BehaviorEngine,
|
||||
):
|
||||
return
|
||||
try:
|
||||
await asyncio.to_thread(service.reconcile_all, "startup")
|
||||
await asyncio.to_thread(engine.train_all)
|
||||
await asyncio.to_thread(engine.evaluate_all)
|
||||
logger.info("Startup-Reconciliation erfolgreich abgeschlossen.")
|
||||
return
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Startup-Reconciliation verschoben: %s. Neuer Versuch in %ss.",
|
||||
exc,
|
||||
delay_seconds,
|
||||
)
|
||||
await asyncio.sleep(delay_seconds)
|
||||
delay_seconds = min(delay_seconds * 2, 60)
|
||||
|
||||
|
||||
async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
||||
@@ -243,7 +275,11 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
||||
logger.warning("Ungültige JSON-Nachricht von HA-WebSocket")
|
||||
except Exception as exc:
|
||||
logger.exception("Fehler bei Event-Verarbeitung: %s", exc)
|
||||
except (websockets.exceptions.ConnectionClosed, OSError) as exc:
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosed,
|
||||
websockets.exceptions.InvalidStatus,
|
||||
OSError,
|
||||
) as exc:
|
||||
logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 5s...", exc)
|
||||
if ws_status is not None:
|
||||
ws_status.status = "reconnecting"
|
||||
@@ -280,7 +316,10 @@ async def _fallback_prediction(app: FastAPI) -> None:
|
||||
"Fallback-Vorhersage aktiv (WebSocket-Status: %s)",
|
||||
ws_status.status if ws_status else "unavailable",
|
||||
)
|
||||
await asyncio.to_thread(engine.evaluate_all)
|
||||
try:
|
||||
await asyncio.to_thread(engine.evaluate_all)
|
||||
except Exception:
|
||||
logger.exception("Fallback-Vorhersage fehlgeschlagen.")
|
||||
|
||||
|
||||
def _load_ha_state_cache(reader: HaReader) -> dict[str, HaEntitySummary]:
|
||||
|
||||
@@ -305,11 +305,8 @@ async function loadOverview() {
|
||||
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
chips.innerHTML = "";
|
||||
}
|
||||
await Promise.all([
|
||||
loadActuatorDiscovery(),
|
||||
loadActuatorSuggestions(),
|
||||
loadConfiguredActuators(),
|
||||
]);
|
||||
await Promise.all([loadActuatorDiscovery(), loadConfiguredActuators()]);
|
||||
void loadActuatorSuggestions();
|
||||
}
|
||||
|
||||
async function loadActuatorDiscovery() {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "sillyhome-next"
|
||||
version = "0.7.13"
|
||||
version = "0.7.15"
|
||||
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user