# EMS — Energie Management System ## Project overview Eigenverbrauchsoptimiertes Energie Management System für eine Heim-PV-Anlage. Liest Zustandsdaten aus einem bestehenden Prometheus, trifft Schaltentscheidungen basierend auf SOC-Schwellen und Hysterese, und steuert Shelly-Aktoren per HTTP. ## Architecture ``` Prometheus (192.168.0.23:9090) → Collector (PromQL) → Decision Engine → Actuator → Shelly HTTP ↓ Prometheus Exporter (:9099) ``` ### Layers - **Collector** (`internal/collector/`): Reads system state via PromQL instant queries from existing Prometheus - **Engine** (`internal/engine/`): Pure decision logic — SOC gates, hysteresis, priority ordering. No side effects, fully testable. - **Actuator** (`internal/actuator/`): Executes switching via Shelly Gen1/Gen2 HTTP APIs - **Metrics** (`internal/metrics/`): Exports EMS decisions as Prometheus metrics on :9099 ### Key design decisions - Data reads come from Prometheus (already scraped every 2min by custom Viessmann exporter) - Data writes (Shelly switching, Viessmann API for WW) are done directly by the EMS - `pcc_transfer_power_exchange_value`: positive = grid import, negative = grid export - Decision engine is pure: `func Decide(state, now) → []Action` — easy to unit test - All thresholds configurable via YAML (`configs/ems-config.yaml`) ## Hardware setup | Component | Details | |---|---| | PV | 7 kWp, Trina VX3 4.6 hybrid inverter | | Battery | 8 kWh, managed autonomously by VX3 | | Heat pump | Viessmann Vitocal 200-S AWB-E-AC | | SG-Ready | Shelly 1 Gen1 @ 192.168.42.90 → heating buffer +5°C | | Wallbox A | 2kW, Shelly Plus 1 PM Gen2 @ 192.168.42.107 | | Wallbox B | 4kW, Shelly Plus 1 Gen2 @ 192.168.42.51 | | WW control | Viessmann API (OAuth2, refresh token) — NOT YET IMPLEMENTED | ## Prometheus metrics (read from existing instance) - `pcc_transfer_power_exchange_value` — grid power (pos=import, neg=export) - `ess_stateOfCharge_value` — battery SOC (0-100%) - `photovoltaic_production_current_value` — current PV production (W) - `ess_power_value` — battery charge/discharge (W) - `heating_compressors_0_power_value` — heat pump compressor power (W) - `heating_sensors_temperature_outside_value` — outdoor temp (°C) ## Decision logic summary 1. SOC gates: <50% all blocked, 50-70% SG-Ready only, 70-90% +WB_A, >90% all 2. Export threshold + hysteresis (4min on, 6min off) 3. Priority: SG-Ready (heating only) → WB_A (2kW) → WB_B (4kW) 4. Shutdown: reverse order, respecting min runtimes (15min WB, 30min SG-Ready) 5. Emergency brake: SOC drops below threshold → immediate shutdown ## TODO / open items - [ ] Viessmann API OAuth2 write integration (WW-Solltemperatur) - [ ] PV Forecast integration (Solcast/forecast.solar) for strategic layer - [ ] Modbus TCP/RS485 for faster PCC data (Phase 3) - [ ] Grafana dashboard for EMS metrics - [ ] go.sum generation (`go mod tidy`) - [ ] Integration tests with mock Prometheus / mock Shelly endpoints ## Commands ```bash make build # Build binary make test # Run unit tests make dry-run # Run without switching (log only) make run # Run with live switching make install # Install as systemd service ``` ## Code style - Go 1.23+, structured logging via `log/slog` - Idiomatic Go: no frameworks, standard library where possible - Engine is pure (no I/O) — all side effects in Collector and Actuator - Config via YAML, parsed with gopkg.in/yaml.v3 - Prometheus metrics via prometheus/client_golang