Compare commits
2 Commits
v0.7.16
...
fix/perf-a
| Author | SHA1 | Date | |
|---|---|---|---|
| 058c5dd015 | |||
| 9ddb065f62 |
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.7.17 - 2026-06-16
|
||||||
|
- WebSocket-Eventpfad ist schneller: irrelevante HA-State-Changes werden vor
|
||||||
|
dem teuren State-Cache-Listenbau verworfen.
|
||||||
|
- WebSocket nutzt Keepalive und reconnectet nach Abbrüchen nach 1s statt 5s.
|
||||||
|
|
||||||
## 0.7.16 - 2026-06-16
|
## 0.7.16 - 2026-06-16
|
||||||
- Beobachtete Aktoren werden in der Übersicht nach Raum oder Typ gruppiert und
|
- Beobachtete Aktoren werden in der Übersicht nach Raum oder Typ gruppiert und
|
||||||
mit Friendly Name angezeigt.
|
mit Friendly Name angezeigt.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: SillyHome Next
|
name: SillyHome Next
|
||||||
version: "0.7.16"
|
version: "0.7.17"
|
||||||
slug: sillyhome_next
|
slug: sillyhome_next
|
||||||
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
|
||||||
url: http://192.168.6.31:3000/pino/sillyhome-next
|
url: http://192.168.6.31:3000/pino/sillyhome-next
|
||||||
|
|||||||
27
app/main.py
27
app/main.py
@@ -105,7 +105,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SillyHome Next API",
|
title="SillyHome Next API",
|
||||||
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
||||||
version="0.7.16",
|
version="0.7.17",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
app.state.settings = load_settings()
|
app.state.settings = load_settings()
|
||||||
@@ -211,7 +211,11 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
|||||||
if ws_status is not None:
|
if ws_status is not None:
|
||||||
ws_status.status = "connecting"
|
ws_status.status = "connecting"
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(ws_url, ping_interval=None) as websocket:
|
async with websockets.connect(
|
||||||
|
ws_url,
|
||||||
|
ping_interval=20,
|
||||||
|
ping_timeout=10,
|
||||||
|
) as websocket:
|
||||||
auth_required_msg = await websocket.recv()
|
auth_required_msg = await websocket.recv()
|
||||||
auth_required_data = json.loads(auth_required_msg)
|
auth_required_data = json.loads(auth_required_msg)
|
||||||
if auth_required_data.get("type") != "auth_required":
|
if auth_required_data.get("type") != "auth_required":
|
||||||
@@ -263,6 +267,8 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
|||||||
continue
|
continue
|
||||||
new_state = event_data.get("new_state")
|
new_state = event_data.get("new_state")
|
||||||
_update_ha_state_cache(state_cache, entity_id, new_state)
|
_update_ha_state_cache(state_cache, entity_id, new_state)
|
||||||
|
if not _is_relevant_state_change(store, str(entity_id)):
|
||||||
|
continue
|
||||||
# Prüfe, ob Entity ein Aktor oder relevanter Kontext ist
|
# Prüfe, ob Entity ein Aktor oder relevanter Kontext ist
|
||||||
# Sofortige Vorhersage für betroffene Aktoren auslösen
|
# Sofortige Vorhersage für betroffene Aktoren auslösen
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
@@ -280,17 +286,17 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None:
|
|||||||
websockets.exceptions.InvalidStatus,
|
websockets.exceptions.InvalidStatus,
|
||||||
OSError,
|
OSError,
|
||||||
) as exc:
|
) as exc:
|
||||||
logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 5s...", exc)
|
logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 1s...", exc)
|
||||||
if ws_status is not None:
|
if ws_status is not None:
|
||||||
ws_status.status = "reconnecting"
|
ws_status.status = "reconnecting"
|
||||||
ws_status.error = str(exc)
|
ws_status.error = str(exc)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(1)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("Unerwarteter Fehler im Event-Listener: %s", exc)
|
logger.exception("Unerwarteter Fehler im Event-Listener: %s", exc)
|
||||||
if ws_status is not None:
|
if ws_status is not None:
|
||||||
ws_status.status = "error"
|
ws_status.status = "error"
|
||||||
ws_status.error = str(exc)
|
ws_status.error = str(exc)
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
# Fallback: periodische Vorhersage falls Event-Stream ausfällt
|
# Fallback: periodische Vorhersage falls Event-Stream ausfällt
|
||||||
@@ -341,6 +347,17 @@ def _update_ha_state_cache(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_relevant_state_change(store: ActuatorStore, entity_id: str) -> bool:
|
||||||
|
for record in store.list():
|
||||||
|
if record.actuator_entity_id == entity_id:
|
||||||
|
return True
|
||||||
|
if record.assignment.selected_numeric_entity_id == entity_id:
|
||||||
|
return True
|
||||||
|
if entity_id in record.assignment.selected_context_entity_ids:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _ha_entity_from_event(
|
def _ha_entity_from_event(
|
||||||
entity_id: str,
|
entity_id: str,
|
||||||
new_state: dict[str, object],
|
new_state: dict[str, object],
|
||||||
|
|||||||
@@ -285,14 +285,15 @@ function optionGroups(entities, selectedIds = new Set()) {
|
|||||||
async function loadOverview() {
|
async function loadOverview() {
|
||||||
const status = document.getElementById("status");
|
const status = document.getElementById("status");
|
||||||
const chips = document.getElementById("status-chips");
|
const chips = document.getElementById("status-chips");
|
||||||
|
let reconciliation = null;
|
||||||
try {
|
try {
|
||||||
const [health, websocket, ml, reconciliation, actuators] = await Promise.all([
|
const [health, websocket, ml] = await Promise.all([
|
||||||
api("health"),
|
api("health"),
|
||||||
api("health/websocket"),
|
api("health/websocket"),
|
||||||
api("ml/health"),
|
api("ml/health"),
|
||||||
api("v1/actuators/reconciliation/state"),
|
|
||||||
api("v1/actuators"),
|
|
||||||
]);
|
]);
|
||||||
|
reconciliation = await api("v1/actuators/reconciliation/state");
|
||||||
|
const actuators = await api("v1/actuators");
|
||||||
status.innerHTML = `<p class="ok">System bereit</p><p>Letzte automatische Prüfung: ${escapeHtml(reconciliation.last_completed_at || "noch nie")}</p>`;
|
status.innerHTML = `<p class="ok">System bereit</p><p>Letzte automatische Prüfung: ${escapeHtml(reconciliation.last_completed_at || "noch nie")}</p>`;
|
||||||
chips.innerHTML = [
|
chips.innerHTML = [
|
||||||
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
||||||
@@ -313,12 +314,8 @@ async function loadActuatorDiscovery() {
|
|||||||
const options = document.getElementById("actuator-options");
|
const options = document.getElementById("actuator-options");
|
||||||
const select = document.getElementById("actuator-select");
|
const select = document.getElementById("actuator-select");
|
||||||
try {
|
try {
|
||||||
const [available, configured] = await Promise.all([
|
const available = await api("v1/actuators/discovery");
|
||||||
api("v1/actuators/discovery"),
|
actuatorChoices = available;
|
||||||
api("v1/actuators"),
|
|
||||||
]);
|
|
||||||
const configuredIds = new Set(configured.map(record => record.actuator_entity_id));
|
|
||||||
actuatorChoices = available.filter(entity => !configuredIds.has(entity.entity_id));
|
|
||||||
options.innerHTML = actuatorChoices.slice(0, 120).map(entity =>
|
options.innerHTML = actuatorChoices.slice(0, 120).map(entity =>
|
||||||
`<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.friendly_name || entity.entity_id)}${entity.area_name ? ` (${escapeHtml(entity.area_name)})` : ""}</option>`
|
`<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.friendly_name || entity.entity_id)}${entity.area_name ? ` (${escapeHtml(entity.area_name)})` : ""}</option>`
|
||||||
).join("");
|
).join("");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "sillyhome-next"
|
name = "sillyhome-next"
|
||||||
version = "0.7.16"
|
version = "0.7.17"
|
||||||
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
connect.assert_called_once_with(
|
connect.assert_called_once_with(
|
||||||
"ws://homeassistant:8123/api/websocket",
|
"ws://homeassistant:8123/api/websocket",
|
||||||
ping_interval=None,
|
ping_interval=20,
|
||||||
|
ping_timeout=10,
|
||||||
)
|
)
|
||||||
assert fake_ws.sent == [
|
assert fake_ws.sent == [
|
||||||
{"type": "auth", "access_token": "test-token"},
|
{"type": "auth", "access_token": "test-token"},
|
||||||
@@ -110,6 +111,7 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None:
|
|||||||
mock_app.state.behavior_engine = mock_engine
|
mock_app.state.behavior_engine = mock_engine
|
||||||
mock_app.state.ha_reader = _FakeHaReader()
|
mock_app.state.ha_reader = _FakeHaReader()
|
||||||
mock_store = ActuatorStore(tmp_path / "store")
|
mock_store = ActuatorStore(tmp_path / "store")
|
||||||
|
mock_store.configure("light.test")
|
||||||
mock_app.state.actuator_store = mock_store
|
mock_app.state.actuator_store = mock_store
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user