drop obsolete files
This commit is contained in:
@@ -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.
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
- 3–4 Spalten auf größeren Screens
|
||||
- Kachelgröße kompakt (ca. 140px x 110px)
|
||||
- Icon ~32px
|
||||
- Text ~14–16px
|
||||
- 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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user