Complete self-consumption optimisation system for 7 kWp PV installation: - Prometheus collector (grid power, SOC, PV, per-phase, compressor) - Pure decision engine with SOC gates, hysteresis, priority ordering - Shelly Gen1/Gen2 actuator (SHA-256 Digest auth, PM power readback) - Viessmann OAuth2 client for DHW temperature control - PV forecast integration (forecast.solar) - Wallbox mutual exclusion (VX3 4.6 kW AC output constraint) - Car-not-charging detection via Shelly PM - Compressor idle → early SG-Ready release - Per-phase grid power for single-phase wallbox decisions - Manual override detection and web UI with override buttons - Full unit test coverage for decision engine - systemd service, Makefile, complete documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.5 KiB
3.5 KiB
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
- SOC gates: <50% all blocked, 50-70% SG-Ready only, 70-90% +WB_A, >90% all
- Export threshold + hysteresis (4min on, 6min off)
- Priority: SG-Ready (heating only) → WB_A (2kW) → WB_B (4kW)
- Shutdown: reverse order, respecting min runtimes (15min WB, 30min SG-Ready)
- 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
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