1. SOC high bypass: when SOC >= soc_high_bypass_pct (default 90%), activate car charging without requiring a good forecast. Solves today's missed charging window where 4kW was exported for 6h with a full battery because the intraday forecast dropped from 15.4 to 13.3 kWh. 2. Log ambient_c in every cycle: makes it diagnosable why isHeatingPeriod suppresses SG-Ready on warm days (heating_min_ambient_c: 15°C gate). 3. Consumer names in logs: replace slog integer Consumer values with .String() so logs show "wallbox_a" instead of "2". 4. Status page order: reorder consumers by EMS priority (WallboxA → WallboxB → WW → SG-Ready) instead of the old reversed order. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
7.2 KiB
YAML
148 lines
7.2 KiB
YAML
# 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_sensors_power_value * heating_compressors_0_power_value * 10" # actual watts: sensors(%) × rated_kW(10) × 10
|
||
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)
|
||
ww_top_temp: "heating_dhw_sensors_temperature_dhwCylinder_top_value" # DHW cylinder top temperature (°C)
|
||
|
||
# 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_max_setpoint_c: 60 # absolute maximum WW setpoint (°C)
|
||
ww_hysteresis_c: 5 # Viessmann switchOn = setpoint - hysteresis (°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: 500 # heat pump compressor below this = idle (W); running starts at ~2700W, so 500W cleanly separates idle (0W) from running
|
||
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)
|
||
wallbox_a_accepted_import_w: 0 # tolerate this much grid import while WallboxA is running (0 = disabled; WallboxA is 2kW, inverter headroom is sufficient)
|
||
wallbox_b_accepted_import_w: 600 # tolerate up to 600W import while WallboxB runs (4kW wallbox + 450W house > 4.6kW inverter max on PV dips)
|
||
|
||
# Season detection
|
||
season:
|
||
heating_start_month: 10 # October
|
||
heating_end_month: 4 # April
|
||
heating_min_ambient_c: 15 # above 15°C outdoor temp: no SG-Ready even within heating months
|
||
|
||
# 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
|
||
fetch_interval: "4h" # re-fetch during the day (free tier: 12 req/day → 3 fetches)
|
||
fetch_window_start: "07:00"
|
||
fetch_window_end: "19:00"
|
||
base_load_w: 450 # steady-state house consumption (W) — spikes from kettle/dishwasher are hourly averages and negligible
|
||
min_surplus_w: 1800 # min export surplus to trigger wallbox — matches wallbox_a threshold
|
||
|
||
# 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" # default lockout when EMS detects external Shelly change
|
||
override_max_import_w: 800 # cancel override immediately if importing more than this (0 = disabled)
|
||
monitor_only_file: "/etc/ems/monitor-only" # presence of this file = monitor-only mode active
|
||
ww_boost_disable_file: "/etc/ems/ww-boost-off" # presence of this file = WW boost disabled
|
||
trip_goal_file: "/var/lib/ems/trip-goal.json" # persisted active trip goal
|
||
session_log_file: "/var/lib/ems/sessions.jsonl" # JSONL log of completed charge sessions
|
||
|
||
# Battery physical properties (for EOD soft-stop calculation)
|
||
battery:
|
||
capacity_kwh: 8.0 # usable battery capacity (kWh)
|
||
|
||
# Proactive car charging strategy (forecast-driven, no export threshold required)
|
||
car_charging:
|
||
min_soc: 25 # start car charging only if battery SOC ≥ this (%)
|
||
soc_floor: 5 # emergency brake floor — stop if SOC drops below this (%)
|
||
soc_high_bypass_pct: 90 # activate without forecast check when SOC ≥ this % (battery full → always charge)
|
||
eod_soc_target: 90 # desired battery SOC at end of day / sunset (%)
|
||
eod_time: "16:00" # start EOD soft-stop checks after this time
|
||
no_car_retry_min: 30 # minutes before retrying after no-car detection
|
||
pv_threshold_a_w: 1000 # min PV production to start WallboxA (W) — 50% of 2kW rated
|
||
pv_threshold_b_w: 2000 # min PV production to start WallboxB (W) — 50% of 4kW rated
|
||
grid_delta_thresh_w: 2000 # min grid power shift after WallboxB activation = car detected (W)
|
||
|
||
# Known car profiles for trip mode
|
||
cars:
|
||
mini:
|
||
name: "Mini Cooper SE"
|
||
battery_kwh: 50.0
|
||
bmw:
|
||
name: "BMW ix2 eDrive20"
|
||
battery_kwh: 63.0
|