drop obsolete files

This commit is contained in:
2025-11-20 21:50:43 +01:00
parent 57b4d7d762
commit 19a3dfdd65
18 changed files with 0 additions and 2842 deletions

View File

@@ -1,371 +0,0 @@
# Rule Interface Documentation
## Overview
The rule interface provides a clean abstraction for implementing automation rules. Rules respond to device state changes and can publish commands, persist state, and log diagnostics.
## Core Components
### 1. RuleDescriptor
Configuration data for a rule instance (loaded from `rules.yaml`):
```python
RuleDescriptor(
id="window_setback_wohnzimmer", # Unique rule ID
name="Fensterabsenkung Wohnzimmer", # Optional display name
type="window_setback@1.0", # Rule type + version
targets={ # Rule-specific targets
"rooms": ["Wohnzimmer"],
"contacts": ["kontakt_wohnzimmer_..."],
"thermostats": ["thermostat_wohnzimmer"]
},
params={ # Rule-specific parameters
"eco_target": 16.0,
"open_min_secs": 20
}
)
```
### 2. RedisState
Async state persistence with automatic reconnection and retry logic:
```python
# Initialize (done by rule engine)
redis_state = RedisState("redis://172.23.1.116:6379/8")
# Simple key-value with TTL
await ctx.redis.set("rules:my_rule:temp", "22.5", ttl_secs=3600)
value = await ctx.redis.get("rules:my_rule:temp") # Returns "22.5" or None
# Hash storage (for multiple related values)
await ctx.redis.hset("rules:my_rule:sensors", "bedroom", "open")
await ctx.redis.hset("rules:my_rule:sensors", "kitchen", "closed")
value = await ctx.redis.hget("rules:my_rule:sensors", "bedroom") # "open"
# TTL management
await ctx.redis.expire("rules:my_rule:temp", 7200) # Extend to 2 hours
# JSON helpers (for complex data)
import json
data = {"temp": 22.5, "humidity": 45}
await ctx.redis.set("rules:my_rule:data", ctx.redis._dumps(data))
stored = await ctx.redis.get("rules:my_rule:data")
parsed = ctx.redis._loads(stored) if stored else None
```
**Key Conventions:**
- Use prefix `rules:{rule_id}:` for all keys
- Example: `rules:window_setback_wohnzimmer:thermo:device_123:previous`
- TTL recommended for temporary state (previous temperatures, timers)
**Robustness Features:**
- Automatic retry with exponential backoff (default: 3 retries)
- Connection pooling (max 10 connections)
- Automatic reconnection on Redis restart
- Health checks every 30 seconds
- All operations wait and retry, no exceptions on temporary outages
### 3. MQTTClient
Async MQTT client with event normalization and command publishing:
```python
# Initialize (done by rule engine)
mqtt_client = MQTTClient(
broker="172.16.2.16",
port=1883,
client_id="rule_engine"
)
# Subscribe and receive normalized events
async for event in mqtt_client.connect():
# Event structure:
# {
# "topic": "home/contact/sensor_1/state",
# "type": "state",
# "cap": "contact", # Capability (contact, thermostat, etc.)
# "device_id": "sensor_1",
# "payload": {"contact": "open"},
# "ts": "2025-11-11T10:30:45.123456"
# }
if event['cap'] == 'contact':
handle_contact(event)
elif event['cap'] == 'thermostat':
handle_thermostat(event)
# Publish commands (within async context)
await mqtt_client.publish_set_thermostat("thermostat_id", 22.5)
```
**Subscriptions:**
- `home/contact/+/state` - All contact sensor state changes
- `home/thermostat/+/state` - All thermostat state changes
**Publishing:**
- Topic: `home/thermostat/{device_id}/set`
- Payload: `{"type":"thermostat","payload":{"target":22.5}}`
- QoS: 1 (at least once delivery)
**Robustness:**
- Automatic reconnection with exponential backoff
- Connection logging (connect/disconnect events)
- Clean session handling
### 4. MQTTPublisher (Legacy)
Simplified wrapper around MQTTClient for backward compatibility:
```python
# Set thermostat temperature
await ctx.mqtt.publish_set_thermostat("thermostat_wohnzimmer", 21.5)
```
### 5. RuleContext
Runtime context provided to rules:
```python
class RuleContext:
logger # Logger instance
mqtt # MQTTPublisher
redis # RedisState
now() -> datetime # Current timestamp
```
### 5. Rule Abstract Base Class
All rules extend this:
```python
class MyRule(Rule):
async def on_event(self, evt: dict, desc: RuleDescriptor, ctx: RuleContext) -> None:
# Event structure:
# {
# "topic": "home/contact/device_id/state",
# "type": "state",
# "cap": "contact",
# "device_id": "kontakt_wohnzimmer",
# "payload": {"contact": "open"},
# "ts": "2025-11-11T10:30:45.123456"
# }
device_id = evt['device_id']
cap = evt['cap']
if cap == 'contact':
contact_state = evt['payload'].get('contact')
# ... implement logic
```
## Implementing a New Rule
### Step 1: Create Rule Class
```python
from packages.rule_interface import Rule, RuleDescriptor, RuleContext
from typing import Any
class MyCustomRule(Rule):
"""My custom automation rule."""
async def on_event(
self,
evt: dict[str, Any],
desc: RuleDescriptor,
ctx: RuleContext
) -> None:
"""Process device state changes."""
# 1. Extract event data
device_id = evt['device_id']
cap = evt['cap']
payload = evt['payload']
# 2. Filter to relevant devices
if device_id not in desc.targets.get('my_devices', []):
return
# 3. Implement logic
if cap == 'contact':
if payload.get('contact') == 'open':
# Do something
await ctx.mqtt.publish_set_thermostat(
'some_thermostat',
desc.params.get('temp', 20.0)
)
# 4. Persist state if needed
state_key = f"rule:{desc.id}:device:{device_id}:state"
await ctx.redis.set(state_key, payload.get('contact'))
```
### Step 2: Register in RULE_IMPLEMENTATIONS
```python
# In your rule module (e.g., my_custom_rule.py)
RULE_IMPLEMENTATIONS = {
'my_custom@1.0': MyCustomRule,
}
```
### Step 3: Configure in rules.yaml
```yaml
rules:
- id: my_custom_living_room
name: My Custom Rule for Living Room
type: my_custom@1.0
targets:
my_devices:
- device_1
- device_2
params:
temp: 22.0
duration_secs: 300
```
## Best Practices
### Idempotency
Rules MUST be idempotent - processing the same event multiple times should be safe:
```python
# Good: Idempotent
async def on_event(self, evt, desc, ctx):
if evt['payload'].get('contact') == 'open':
await ctx.mqtt.publish_set_thermostat('thermo', 16.0)
# Bad: Not idempotent (increments counter)
async def on_event(self, evt, desc, ctx):
counter = await ctx.redis.get('counter') or '0'
await ctx.redis.set('counter', str(int(counter) + 1))
```
### Error Handling
Handle errors gracefully - the engine will catch and log exceptions:
```python
async def on_event(self, evt, desc, ctx):
try:
await ctx.mqtt.publish_set_thermostat('thermo', 16.0)
except Exception as e:
ctx.logger.error(f"Failed to set thermostat: {e}")
# Don't raise - let event processing continue
```
### State Keys
Use consistent naming for Redis keys:
```python
# Pattern: rule:{rule_id}:{category}:{device_id}:{field}
state_key = f"rule:{desc.id}:contact:{device_id}:state"
ts_key = f"rule:{desc.id}:contact:{device_id}:ts"
prev_key = f"rule:{desc.id}:thermo:{device_id}:previous"
```
### Logging
Use appropriate log levels:
```python
ctx.logger.debug("Detailed diagnostic info")
ctx.logger.info("Normal operation milestones")
ctx.logger.warning("Unexpected but handled situations")
ctx.logger.error("Errors that prevent operation")
```
## Event Structure Reference
### Contact Sensor Event
```python
{
"topic": "home/contact/kontakt_wohnzimmer/state",
"type": "state",
"cap": "contact",
"device_id": "kontakt_wohnzimmer",
"payload": {
"contact": "open" # or "closed"
},
"ts": "2025-11-11T10:30:45.123456"
}
```
### Thermostat Event
```python
{
"topic": "home/thermostat/thermostat_wohnzimmer/state",
"type": "state",
"cap": "thermostat",
"device_id": "thermostat_wohnzimmer",
"payload": {
"target": 21.0,
"current": 20.5,
"mode": "heat"
},
"ts": "2025-11-11T10:30:45.123456"
}
```
## Testing Rules
Rules can be tested independently of the engine:
```python
import pytest
from unittest.mock import AsyncMock, MagicMock
from packages.my_custom_rule import MyCustomRule
from packages.rule_interface import RuleDescriptor, RuleContext
@pytest.mark.asyncio
async def test_my_rule():
# Setup
rule = MyCustomRule()
desc = RuleDescriptor(
id="test_rule",
type="my_custom@1.0",
targets={"my_devices": ["device_1"]},
params={"temp": 22.0}
)
# Mock context
ctx = RuleContext(
logger=MagicMock(),
mqtt_publisher=AsyncMock(),
redis_state=AsyncMock(),
now_fn=lambda: datetime.now()
)
# Test event
evt = {
"device_id": "device_1",
"cap": "contact",
"payload": {"contact": "open"},
"ts": "2025-11-11T10:30:45.123456"
}
# Execute
await rule.on_event(evt, desc, ctx)
# Assert
ctx.mqtt.publish_set_thermostat.assert_called_once_with('some_thermostat', 22.0)
```
## Extension Points
The interface is designed to be extended without modifying the engine:
1. **New rule types**: Just implement `Rule` and register in `RULE_IMPLEMENTATIONS`
2. **New MQTT commands**: Extend `MQTTPublisher` with new methods
3. **New state backends**: Implement `RedisState` interface with different storage
4. **Custom context**: Extend `RuleContext` with additional utilities
The engine only depends on the abstract interfaces, not specific implementations.

View File

@@ -1,171 +0,0 @@
# UI Service - Docker
FastAPI + Jinja2 + HTMX Dashboard für Home Automation
## Build
```bash
docker build -t ui:dev -f apps/ui/Dockerfile .
```
## Run
### Lokal
```bash
docker run --rm -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
```
### Docker Compose
```yaml
services:
ui:
build:
context: .
dockerfile: apps/ui/Dockerfile
ports:
- "8002:8002"
environment:
- API_BASE=http://api:8001
depends_on:
- api
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
containers:
- name: ui
image: ui:dev
ports:
- containerPort: 8002
env:
- name: API_BASE
value: "http://api-service:8001"
livenessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: ui-service
spec:
selector:
app: ui
ports:
- protocol: TCP
port: 8002
targetPort: 8002
type: LoadBalancer
```
## Umgebungsvariablen
| Variable | Default | Beschreibung |
|----------|---------|--------------|
| `API_BASE` | `http://api:8001` | URL des API-Services |
| `UI_PORT` | `8002` | Port der UI-Anwendung |
| `PYTHONDONTWRITEBYTECODE` | `1` | Keine .pyc Files |
| `PYTHONUNBUFFERED` | `1` | Unbuffered Output |
## Endpoints
- `GET /` - Dashboard
- `GET /health` - Health Check
- `GET /dashboard` - Dashboard (alias)
## Security
- Container läuft als **non-root** User `app` (UID: 10001)
- Minimales Python 3.11-slim Base Image
- Keine unnötigen System-Pakete
- Health Check integriert
## Features
- ✅ FastAPI Backend
- ✅ Jinja2 Templates
- ✅ HTMX für reactive UI
- ✅ Server-Sent Events (SSE)
- ✅ Responsive Design
- ✅ Docker & Kubernetes ready
- ✅ Health Check Endpoint
- ✅ Non-root Container
- ✅ Configurable API Backend
## Entwicklung
### Lokales Testing
```bash
# Build
docker build -t ui:dev -f apps/ui/Dockerfile .
# Run
docker run -d --name ui-test -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
# Logs
docker logs -f ui-test
# Health Check
curl http://localhost:8002/health
# Cleanup
docker stop ui-test && docker rm ui-test
```
### Tests
```bash
bash /tmp/test_ui_dockerfile.sh
```
## Troubleshooting
### Container startet nicht
```bash
docker logs ui-test
```
### Health Check schlägt fehl
```bash
docker exec ui-test curl http://localhost:8002/health
```
### API_BASE nicht korrekt
```bash
docker logs ui-test 2>&1 | grep "UI using API_BASE"
```
### Non-root Verifizieren
```bash
docker exec ui-test id
# Sollte zeigen: uid=10001(app) gid=10001(app)
```

View File

@@ -1,188 +0,0 @@
/**
Copilot-Aufgabe: Erzeuge eine neue Home-Dashboard-Seite mit Raum-Kacheln.
Ziel:
Die Seite soll alle Räume als kleine Kacheln darstellen. Auf dem iPhone
sollen immer zwei Kacheln nebeneinander passen. Jede Kachel zeigt:
- Raumname
- Icon (z. B. Wohnzimmer, Küche, Bad, etc.) basierend auf room_id oder einem Mapping
- Anzahl der Geräte im Raum
- Optional: Zusammenfassung wichtiger States (z.B. Anzahl offener Fenster, aktive Lichter)
Datenquelle:
- GET /layout → { "rooms": [{ "name": "...", "devices": [...] }] }
(Achtung: rooms ist ein Array, kein Dictionary!)
- GET /devices → Geräteliste für Feature-Checks
Interaktion:
- Beim Klick/Touch auf eine Raum-Kachel → Navigation zu /room/{room_name}
Layout-Anforderungen:
- 2-Spalten-Grid auf kleinen Screens (max-width ~ 600px)
- 34 Spalten auf größeren Screens
- Kachelgröße kompakt (ca. 140px x 110px)
- Icon ~32px
- Text ~1416px
- Responsive via CSS-Grid oder Flexbox
- Minimaler Einsatz von Tailwind (bevorzugt vanilla CSS)
Akzeptanzkriterien:
- Die Seite lädt alle Räume über die API (fetch).
- Räume werden in der Reihenfolge aus layout.yaml angezeigt.
- Jede Kachel zeigt: Icon + Raumname + Geräteanzahl.
- iPhone-Darstellung verifiziert: zwei Kacheln nebeneinander.
- Funktionierende Navigation zu /room/{room_name}.
- Die Komponente ist vollständig lauffähig.
- Fehlerbehandlung bei API-Fehlern implementiert.
*/
/**
Copilot-Aufgabe: Erzeuge eine Geräte-Grid-Ansicht für einen Raum.
Ziel:
Die Seite zeigt alle Geräte, die in diesem Raum laut layout.yaml liegen.
Die Darstellung erfolgt als kompakte Kacheln, ebenfalls 2 Spalten auf iPhone.
Datenquelle:
- GET /layout → Räume + device_id + title
- GET /devices → Typ + Features
- GET /devices/{id}/state (optional zur Initialisierung)
- Live-Updates: SSE /realtime
Auf einer Gerät-Kachel sollen erscheinen:
- passendes Icon (abhängig von type)
- title (aus layout)
- wichtigste Eigenschaft aus dem State:
- light: power on/off oder brightness in %
- thermostat: current temperature
- contact: open/closed
- temp_humidity: temperature und/oder humidity
- outlet: on/off
- cover: position %
Interaktion:
- Klick/Touch → Navigation zu /device/{device_id}
Akzeptanzkriterien:
- Der Raum wird anhand room_id aus der URL geladen.
- Geräte werden über Join(layout, devices) des Raums selektiert.
- Kacheln sind 2-spaltig auf iPhone.
- State wird initial geladen und per SSE aktualisiert.
- Navigation zu /device/{id} funktioniert.
- Icons passend zum Typ generiert.
*/
/**
Copilot-Aufgabe: Erzeuge eine Detailansicht für ein einzelnes Gerät.
Ziel:
Die Seite zeigt:
- Titel des Geräts (title aus layout)
- Raumname
- Gerätetyp
- State-Werte aus GET /devices/{id}/state
- Live-Updates via SSE
- Steuer-Elemente abhängig vom type + features:
- light: toggle, brightness-slider, optional color-picker
- thermostat: target-temp-slider
- outlet: toggle
- contact: nur Anzeige
- temp_humidity: nur Anzeigen von Temperatur/Humidity
- cover: position-slider und open/close/stop Buttons
API-Integration:
- Set-Kommandos senden via POST /devices/{id}/set
- Validierung: Nur unterstützte Features sichtbar machen
UI-Vorgaben:
- Kompakt, aber komplett
- Buttons gut für Touch erreichbar
- Slider in voller Breite
- Werte (temperature, humidity, battery) übersichtlich gruppiert
Akzeptanzkriterien:
- Device wird korrekt geladen (layout + devices + state).
- Steuerung funktioniert (light on/off, brightness, target temp etc.).
- SSE aktualisiert alle angezeigten Werte live.
- Fehler (z. B. POST /set nicht erreichbar) werden UI-seitig angezeigt.
*/
/**
Copilot-Aufgabe: Erzeuge einen API-Client für das UI.
Der Client soll bereitstellen:
- getLayout(): Layout-Daten
- getDevices(): Device-Basisdaten
- getDeviceState(device_id)
- setDeviceState(device_id, type, payload)
- connectRealtime(onEvent): SSE-Listener
Anforderungen:
- API_BASE aus .env oder UI-Konfiguration
- Fehlerbehandlung
- Timeout optional
- Types für:
- Room
- Device
- DeviceState
- RealtimeEvent
Akzeptanzkriterien:
- Der Client ist voll funktionsfähig und wird im UI genutzt.
- Ein Hook useRealtime(device_id) wird erzeugt.
- Ein Hook useRooms() and useDevices() existieren.
*/
/**
Copilot-Aufgabe: Erzeuge das UI-Routing.
Routen:
- "/" → Home (Räume)
- "/room/:roomId" → RoomView
- "/device/:deviceId" → DeviceView
Anforderungen:
- React Router v6 oder v7
- Layout-Komponente optional
- Loading/Fehlerzustände
- Responsive Verhalten beibehalten
Akzeptanzkriterien:
- Navigation funktioniert zwischen allen Seiten.
- Browser-Back funktioniert erwartungsgemäß.
- Routes unterstützen Refresh ohne Fehler.
*/
/**
Copilot-Aufgabe: Implementiere einen React-Hook useRealtime(deviceId: string | null).
Ziel:
- SSE-Stream /realtime abonnieren
- Nur Events für deviceId liefern
- onMessage → setState
- automatische Reconnects
- Fehlerlogging
Akzeptanz:
- Der Hook kann in RoomView & DeviceView genutzt werden.
- Live-Updates werden korrekt gemerged.
- Disconnect/Reload funktioniert sauber.
*/
/**
Copilot-Aufgabe: Erzeuge eine Icon-Komponente.
Ziel:
Basierend auf device.type und ggf. features ein passendes SVG ausliefern:
- light → Lightbulb
- thermostat → Thermostat
- contact → Door/Window-Sensor
- temp_humidity → Thermometer+Droplet
- outlet → Power-Plug
- cover → Blinds/Rollershutter
Akzeptanz:
- Icons skalieren sauber
- funktionieren in allen Kachel-Komponenten
*/