Initial commit: EMS — Energie Management System
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>
This commit is contained in:
106
configs/ems-config.yaml
Normal file
106
configs/ems-config.yaml
Normal file
@@ -0,0 +1,106 @@
|
||||
# EMS Configuration
|
||||
# All thresholds and timing parameters are configurable here.
|
||||
|
||||
prometheus:
|
||||
url: "http://192.168.0.23:9090"
|
||||
|
||||
# Metric names from the custom Viessmann exporter
|
||||
metrics:
|
||||
grid_power_exchange: "pcc_transfer_power_exchange_value" # positive = import, negative = export
|
||||
battery_soc: "ess_stateOfCharge_value" # 0-100%
|
||||
pv_production: "photovoltaic_production_current_value" # watts
|
||||
battery_power: "ess_power_value" # watts
|
||||
compressor_power: "heating_compressors_0_power_value" # watts
|
||||
ambient_temp: "heating_sensors_temperature_outside_value" # °C
|
||||
phase_l1_power: "pcc_ac_active_power_phaseOne" # per-phase grid power L1 (W)
|
||||
phase_l2_power: "pcc_ac_active_power_phaseTwo" # per-phase grid power L2 (W)
|
||||
phase_l3_power: "pcc_ac_active_power_phaseThree" # per-phase grid power L3 (W)
|
||||
|
||||
# Shelly actuators
|
||||
shelly:
|
||||
sg_ready:
|
||||
ip: "192.168.42.90"
|
||||
gen: 1 # Gen1: /relay/0?turn=on|off
|
||||
wallbox_a:
|
||||
ip: "192.168.42.185"
|
||||
gen: 2 # Gen2: /rpc/Switch.Set
|
||||
power_w: 2000
|
||||
password: "" # set Shelly admin password if auth is enabled
|
||||
wallbox_b:
|
||||
ip: "192.168.42.51"
|
||||
gen: 2 # Gen2: /rpc/Switch.Set
|
||||
power_w: 4000
|
||||
password: "" # set Shelly admin password if auth is enabled
|
||||
|
||||
# Viessmann API (write access for WW temperature)
|
||||
viessmann:
|
||||
token_file: "/etc/ems/viessmann-token.json" # OAuth2 refresh token
|
||||
client_id: "5d1235548dfaa01ce37db8f865d2df4d"
|
||||
installation_id: "1253124"
|
||||
gateway_serial: "7637415006796210"
|
||||
device_id: "0"
|
||||
|
||||
# SOC thresholds — which consumers are allowed at which battery level
|
||||
soc_thresholds:
|
||||
block_all: 50 # below 50%: no consumers
|
||||
sg_ready_only: 70 # 50-70%: only SG-Ready (heating period)
|
||||
plus_wallbox_a: 90 # 70-90%: + Wallbox A (2kW)
|
||||
all_consumers: 90 # above 90%: all consumers
|
||||
|
||||
# Hysteresis settings
|
||||
hysteresis:
|
||||
export_on_duration: "4m" # export must persist for 4 min before switching on
|
||||
import_off_duration: "6m" # import must persist for 6 min before switching off
|
||||
min_runtime_wallbox: "15m"
|
||||
min_runtime_sg_ready: "30m"
|
||||
|
||||
# Power thresholds (grid_power_exchange is positive=import, so we check for negative values)
|
||||
# These are the NEGATIVE thresholds (export) that trigger consumers
|
||||
thresholds:
|
||||
sg_ready_export_w: -500 # export > 500W → SG-Ready
|
||||
ww_export_w: -500 # export > 500W → WW boost
|
||||
wallbox_a_export_w: -1800 # export > 1800W → Wallbox A (total, fallback if no phase data)
|
||||
wallbox_a_phase_export_w: -1500 # per-phase export needed for single-phase Wallbox A
|
||||
wallbox_b_export_w: -3800 # export > 3800W → Wallbox B
|
||||
import_off_w: 200 # import > 200W → start shutdown timer
|
||||
|
||||
# Strategic layer (PV forecast based, daily)
|
||||
strategic:
|
||||
forecast_high_kwh: 25
|
||||
forecast_mid_kwh: 15
|
||||
ww_base_c: 48 # normal WW setpoint (°C)
|
||||
ww_boost_high_c: 5 # +5°C on high-forecast days (>25 kWh)
|
||||
ww_boost_mid_c: 3 # +3°C on medium-forecast days (>15 kWh)
|
||||
ww_window_start: "12:30" # WW boost only allowed from 12:30
|
||||
ww_window_end: "18:00" # ... until 18:00
|
||||
schedule_on: "08:00"
|
||||
schedule_off: "20:00"
|
||||
|
||||
# Consumer behavior — idle detection thresholds
|
||||
consumers:
|
||||
compressor_idle_w: 50 # heat pump compressor below this = idle (W)
|
||||
wallbox_min_charge_w: 50 # wallbox below this = car not charging (W)
|
||||
idle_cycles: 3 # consecutive idle cycles before early release (~6 min at 2-min poll)
|
||||
|
||||
# Season detection
|
||||
season:
|
||||
heating_start_month: 10 # October
|
||||
heating_end_month: 4 # April
|
||||
|
||||
# PV forecast — forecast.solar (free, no API key required)
|
||||
forecast:
|
||||
enabled: true
|
||||
lat: 48.7839
|
||||
lon: 9.3889
|
||||
declination: 20 # panel tilt in degrees
|
||||
azimuth: 5 # degrees from south (south=0, west=90, east=-90)
|
||||
kwp: 7.0 # installed peak power
|
||||
|
||||
# EMS operational settings
|
||||
ems:
|
||||
poll_interval: "2m"
|
||||
listen_addr: ":9099" # Prometheus metrics endpoint
|
||||
log_level: "info"
|
||||
state_file: "/run/ems/heartbeat" # written each cycle for state recovery
|
||||
recovery_timeout: "1h" # ignore Shelly state if EMS was down longer
|
||||
override_timeout: "1h" # how long a manual Shelly change is respected
|
||||
Reference in New Issue
Block a user