Compare commits

..

2 Commits

Author SHA1 Message Date
76d88dd77f Add dev install docs
Some checks failed
quality / test (3.11) (pull_request) Has been cancelled
quality / test (3.13) (pull_request) Has been cancelled
2026-06-16 21:23:08 +02:00
9ddb065f62 Speed up HA event processing
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
2026-06-16 13:58:58 +02:00
6 changed files with 125 additions and 8 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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],

View File

@@ -0,0 +1,93 @@
# Locale Dev-Umgebung — sillyhome-next-dev
Ziel: Repo lokal klonen, Backend starten und Änderungen entwickeln, ohne das produktive Add-on direkt zu beeinflussen.
## Voraussetzungen
- Git
- Python 3.10+
- `pip`
- Optional: Nginx als Reverse-Proxy
## 1. Repo klonen
```bash
git clone http://192.168.6.31:3000/Otto/sillyhome-next-dev.git
cd sillyhome-next-dev
```
## 2. Abhängigkeiten installieren
```bash
sudo apt-get update
sudo apt-get install -y git python3 python3-pip uvicorn
pip3 install -r requirements.txt
```
## 3. Backend starten (temporär)
```bash
uvicorn app.main:app --reload
```
## 4. Backend dauerhaft per systemd starten
Datei `/etc/systemd/system/sillyhome-dev.service`:
```ini
[Unit]
Description=Sillyhome Next Dev
After=network.target
[Service]
WorkingDirectory=/home/otto/sillyhome-next-dev
Environment="PATH=/home/otto/.local/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/usr/local/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
Aktivieren:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now sillyhome-dev.service
```
## 5. Optional: Nginx Reverse-Proxy
Datei `/etc/nginx/sites-available/sillyhome-dev`:
```nginx
server {
listen 8080;
server_name sillyhome-dev.local;
location / {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
Aktivieren:
```bash
sudo ln -sf /etc/nginx/sites-available/sillyhome-dev /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
## 6. Nur Frontend testen
```bash
cd /home/otto/sillyhome-next-dev/app/static
python3 -m http.server 8080 --directory .
```
Danach im Browser `http://127.0.0.1:8080` öffnen.
## Hinweis für Weiterentwicklung
Dieser Branch ist für lokale Entwicklungsiterationen gedacht. Änderungen hier sollen einzeln reviewt und danach in den passenden Produktiv-Branch übernommen werden.

View File

@@ -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 = [

View File

@@ -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()