commit 87f078ba72150c325a0a0a6c36f5e0f06b56f978 Author: Gints Kiršteins Date: Mon Apr 6 21:09:45 2026 +0300 Initial commit — EMHASS + Deye + NordPool setup - EMHASS config for Deye SUN-12K + 32 kWh battery, NordPool LV - Day-ahead optimization automation (14:30 daily) with real battery SOC init - Forecast vs reality template sensors and dashboard card - Migration log from Domoticz Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..454765e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Contains real HA long-lived token — use config_emhass.yaml as reference +config.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..33084df --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# Smart Home Project — HA + Domoticz Migration + +## Systems + +### Home Assistant (primary, growing) +- URL: http://192.168.66.113:8123 +- MCP: connected via hass-mcp +- Entity naming: snake_case +- Prefer native HA constructs over Jinja2 templates +- Always confirm before deleting automations or entities + +### Domoticz (legacy, stable) +- URL: http://192.168.66.157:8080 +- API: no auth required (trusted IP) +- API endpoint: http://192.168.66.157:8080/json.htm +- Full access granted — changes allowed when explicitly requested + +## Migration Strategy +- Pace: slow, step by step — nothing is broken, no rush +- Approach: replicate one device/automation at a time in HA +- Before migrating anything: query Domoticz API to understand current state +- After migrating: verify HA entity works before suggesting Domoticz removal +- Keep a running log in `migration_log.md` in this directory + +## Workflow Rules +- Always check both systems before suggesting changes +- Prefer additive changes (add to HA) over removing from Domoticz +- When creating automations in HA, follow existing snake_case naming +- Ask before any bulk operations +- Never delete from Domoticz without explicit confirmation, even with full access + + +## Energy System + +### Inverter +- Model: Deye SUN-12K-SG04LP3-EU +- Integration: deye-inverter-mqtt (Docker: ghcr.io/kbialek/deye-inverter-mqtt) +- Data arrives via MQTT → HA sensors + +### Battery +- Capacity: 32 kWh +- Connected to Deye inverter + +### EV Charger +- Brand: OpenEVSE +- Integration: OpenEVSE HA integration + +### Spot Prices +- Integration: NordPool, zone: LV +- Currency: EUR/MWh + +### EMHASS +- Add-on URL: http://192.168.66.113:5000 +- Config file: /config/emhass/config_emhass.yaml +- Goal: minimize electricity bill via solar self-consumption, + battery charge/discharge, and EV charging schedule optimization +- Do not write config_emhass.yaml until sensor discovery is complete + and all critical entity IDs are confirmed diff --git a/README.md b/README.md new file mode 100644 index 0000000..a62b669 --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +# EMHASS Energy Management — Deye + NordPool Setup + +Energy Management for Home Assistant (EMHASS) configured for: +- **Deye SUN-12K-SG04LP3-EU** hybrid inverter +- **32 kWh LiFePO4 battery** +- **NordPool LV** spot prices +- Self-consumption optimization with price arbitrage + +--- + +## System Overview + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Deye Inverter │ │ Home Assistant │ │ NordPool │ +│ (MQTT → HA) │─────▶│ Sensors │◀─────│ Spot Prices │ +│ │ │ │ │ (LV zone) │ +└─────────────────┘ └────────┬────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ EMHASS │ + │ (add-on) │ + │ │ + │ • Day-ahead │ + │ optimization │ + │ • Battery │ + │ scheduling │ + │ • Price │ + │ arbitrage │ + └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │ Forecast Sensors │ + │ • p_pv_forecast │ + │ • p_load_forecast │ + │ • p_grid_forecast│ + │ • soc_batt_forecast│ + └─────────────────┘ +``` + +--- + +## Daily Workflow + +### 1. NordPool Price Publication (~14:30 EET/EEST) + +NordPool publishes next-day spot prices for the LV zone. The `sensor.nordpool_kwh_lv_eur_3_10_021` sensor receives: +- `today` — 96 values (15-min resolution) for current day +- `tomorrow` — 96 values for next day (once published, typically by 14:30) +- `tomorrow_valid` — becomes `true` when prices are available + +### 2. EMHASS Automation (14:30 daily) + +```yaml +trigger: 14:30 + ↓ +wait: tomorrow_valid == true (timeout 60min) + ↓ +build: 48-value price list (30-min resolution) + = NordPool 15-min prices resampled to 30-min steps + (prices already include distribution + VAT — no offset added) +read: battery_soc_init = sensor.deye_inverter_mqtt_battery_soc / 100 + ↓ +POST: /action/dayahead-optim with price list + battery_soc_init + ↓ +wait: 15 seconds + ↓ +POST: /action/publish-data + ↓ +result: Forecast sensors updated with tomorrow's plan +``` + +### 3. What EMHASS Optimizes + +With `costfun: profit` and `set_nodischarge_to_grid: true`: +- **Discharge battery** to cover load during expensive hours (sell to yourself) +- **Charge battery** from grid during cheap hours (price arbitrage) +- **Never export** to grid (`maximum_power_to_grid: 0`) +- Maintain SOC between 10%–100%, target 60% end-of-day + +--- + +## Configuration Files + +### EMHASS Config (`/addon_configs/5b918bf2_emhass/config.json`) + +```json +{ + "hass_url": "http://192.168.66.113:8123", + "long_lived_token": "", + + "costfun": "profit", + "optimization_time_step": 30, + "historic_days_to_retrieve": 10, + + "sensor_power_photovoltaics": "sensor.deye_inverter_mqtt_dc_total_power", + "sensor_power_load_no_var_loads": "sensor.deye_inverter_mqtt_total_load_power", + + "maximum_power_from_grid": 16090, + "maximum_power_to_grid": 0, + + "load_cost_forecast_method": "list", + + "set_use_battery": true, + "battery_nominal_energy_capacity": 32000, + "battery_charge_power_max": 11000, + "battery_discharge_power_max": 11000, + "battery_charge_efficiency": 0.96, + "battery_discharge_efficiency": 0.96, + "battery_minimum_state_of_charge": 0.1, + "battery_maximum_state_of_charge": 1.0, + "battery_target_state_of_charge": 0.6, + + "number_of_deferrable_loads": 0 +} +``` + +### HA Configuration (`/config/configuration.yaml`) + +```yaml +rest_command: + emhass_dayahead_optim: + url: http://192.168.66.113:5000/action/dayahead-optim + method: POST + content_type: application/json + body: "{{ body }}" + emhass_publish_data: + url: http://192.168.66.113:5000/action/publish-data + method: POST + content_type: application/json +``` + +### Automation (`/config/automations.yaml`) + +Located at the end of automations.yaml — see the file for the full Jinja2 template that: +1. Reads `sensor.nordpool_kwh_lv_eur_3_10_021` `tomorrow` attribute +2. Resamples from 96 values (15-min) to 48 values (30-min) +3. Reads real battery SOC and converts to fraction (`battery_soc_init`) +4. Passes both to EMHASS via `load_cost_forecast` + `battery_soc_init` parameters + +Note: NordPool prices already include distribution + VAT — no fixed offset is added. + +--- + +## Key Sensors + +| Role | Entity ID | Notes | +|------|-----------|-------| +| PV Power | `sensor.deye_inverter_mqtt_dc_total_power` | DC total from strings | +| Load Power | `sensor.deye_inverter_mqtt_total_load_power` | House consumption | +| Grid Power | `sensor.deye_inverter_mqtt_total_grid_power` | +import / -export | +| Battery SOC | `sensor.deye_inverter_mqtt_battery_soc` | % | +| Battery Power | `sensor.deye_inverter_mqtt_battery_power` | +discharge / -charge | +| NordPool | `sensor.nordpool_kwh_lv_eur_3_10_021` | Spot price EUR/kWh | +| PV Forecast | `sensor.p_pv_forecast` | EMHASS output | +| Load Forecast | `sensor.p_load_forecast` | EMHASS output | +| Grid Forecast | `sensor.p_grid_forecast` | EMHASS output | +| SOC Forecast | `sensor.soc_batt_forecast` | EMHASS output — planned end-of-day SOC (always = target 60%) | + +--- + +## Sign Conventions + +| Sensor | Positive | Negative | +|--------|----------|----------| +| `battery_power` | Discharging to load | Charging from PV/grid | +| `total_grid_power` | Importing from grid | Exporting to grid | +| `inverter_l*_power` | Export to grid | Import from grid | + +--- + +## Comparison: Forecast vs Reality + +### Template Sensors Created + +| Sensor | Description | +|--------|-------------| +| `sensor.battery_soc_forecast_error` | Actual SOC - Forecast SOC (%) | +| `sensor.current_hour_price` | Current hour's electricity price (€/kWh) | + +### Monitoring Dashboard + +Add the dashboard card from `dashboard_card.yaml`: +1. Go to **Dashboard** → **Edit** → **Add Card** → **Manual** +2. Paste the YAML content + +### Checking Performance + +**Daily comparison questions:** +1. Did battery charge during cheap hours (00:00–05:00, 12:00–14:00)? +2. Did battery discharge during peak hours (19:00–22:00)? +3. Is `sensor.battery_soc_forecast_error` within ±5%? + +**If error > 10%:** +- Check if Deye inverter is overriding EMHASS decisions +- Consider adding enforcement automation (see below) + +### History Graph + +Compare actual vs forecast over time: +```yaml +type: history-graph +entities: + - entity: sensor.deye_inverter_mqtt_battery_soc + name: Actual SOC + - entity: sensor.soc_batt_forecast + name: Forecast SOC +hours_to_show: 48 +``` + +--- + +## Adding EV Charging (OpenEVSE) + +When the EV charger is integrated: + +1. **Create a sensor** for EV power (or use OpenEVSE integration): + ```yaml + # In configuration.yaml + sensor: + - platform: template + sensors: + ev_charger_power: + friendly_name: "EV Charger Power" + value_template: "{{ states('sensor.openevse_current_power') | float(0) * 230 }}" + unit_of_measurement: "W" + ``` + +2. **Create a template sensor** that subtracts EV from total load: + ```yaml + - platform: template + sensors: + load_power_no_var_loads: + friendly_name: "Load Power (no EV)" + value_template: "{{ states('sensor.deye_inverter_mqtt_total_load_power') | float - states('sensor.ev_charger_power') | float(0) }}" + unit_of_measurement: "W" + ``` + +3. **Update EMHASS config**: + ```json + "sensor_power_load_no_var_loads": "sensor.load_power_no_var_loads", + "number_of_deferrable_loads": 1, + "nominal_power_of_deferrable_loads": [7400], + "operating_hours_of_each_deferrable_load": [6] + ``` + +4. **Restart EMHASS** + +--- + +## Troubleshooting + +### EMHASS logs show "config.json does not exist" + +The config file must be at: +``` +/addon_configs/5b918bf2_emhass/config.json +``` + +Not at `/homeassistant/config.json` or `/config/config.json`. + +### "Shape of passed values is (X, 1), indices imply (48, 1)" + +Load sensor has insufficient history. EMHASS needs ≥48 data points (24h at 30-min resolution). Either: +- Wait for more history to accumulate +- Switch to `sensor.deye_inverter_mqtt_total_load_power` (has months of history) + +### Check EMHASS status via SSH + +```bash +ssh root@192.168.66.113 +ha addons logs 5b918bf2_emhass | tail -30 +ha addons restart 5b918bf2_emhass +``` + +### Verify NordPool prices are being passed + +```bash +curl -s -X POST http://192.168.66.113:5000/action/dayahead-optim \ + -H "Content-Type: application/json" \ + -d '{"load_cost_forecast": [0.20, 0.19, ...]}' +``` + +Check logs for: `Passed runtime parameters: {'load_cost_forecast': [...]}` + +--- + +## Files in This Repository + +| File | Purpose | +|------|---------| +| `config.json` | EMHASS configuration (deploy to HA) | +| `config_emhass.yaml` | Human-readable reference (same config) | +| `migration_log.md` | Migration notes from Domoticz | +| `README.md` | This file | + +--- + +## References + +- [EMHASS Documentation](https://emhass.readthedocs.io/) +- [EMHASS GitHub](https://github.com/davidusb-geek/emhass) +- [Deye Inverter MQTT Integration](https://github.com/kbialek/deye-inverter-mqtt) +- [NordPool Integration](https://www.home-assistant.io/integrations/nordpool/) \ No newline at end of file diff --git a/config_emhass.yaml b/config_emhass.yaml new file mode 100644 index 0000000..17b8543 --- /dev/null +++ b/config_emhass.yaml @@ -0,0 +1,63 @@ +# EMHASS v0.17.1 — reference copy of config.json in YAML form +# The actual file deployed to HA is config.json (at /addon_configs/5b918bf2_emhass/config.json) +# This YAML is for human readability only. +# +# System: Deye SUN-12K-SG04LP3-EU + 32 kWh LiFePO4 battery +# Location: Latvia, NordPool LV zone +# Strategy: profit (minimize grid cost), self-consumption, no grid export + +hass_url: http://192.168.66.113:8123 +long_lived_token: "" + +costfun: profit +optimization_time_step: 30 # minutes +historic_days_to_retrieve: 10 +method_ts_round: nearest + +# ── Sensors ────────────────────────────────────────────────────────────────── +sensor_power_photovoltaics: sensor.deye_inverter_mqtt_dc_total_power +sensor_power_load_no_var_loads: sensor.deye_inverter_mqtt_total_load_power +sensor_replace_zero: + - sensor.deye_inverter_mqtt_dc_total_power +sensor_linear_interp: + - sensor.deye_inverter_mqtt_dc_total_power + - sensor.deye_inverter_mqtt_total_load_power + +# ── Grid ───────────────────────────────────────────────────────────────────── +maximum_power_from_grid: 16090 # W — contract limit +maximum_power_to_grid: 0 # W — no export (self-consumption only) +set_total_pv_sell: false +set_nodischarge_to_grid: true # battery never exports to grid +set_nocharge_from_grid: false # allow grid→battery during cheap hours + +# ── Electricity pricing ─────────────────────────────────────────────────────── +# Dynamic NordPool pricing via automation (14:30 daily). +# NordPool prices (passed as list) already include distribution + VAT. +load_cost_forecast_method: list +photovoltaic_production_sell_price: 0.0 # no feed-in tariff + +# ── Inverter ───────────────────────────────────────────────────────────────── +inverter_is_hybrid: true +inverter_ac_output_max: 12000 # W — Deye SUN-12K +inverter_ac_input_max: 12000 # W + +# ── Battery ─────────────────────────────────────────────────────────────────── +set_use_battery: true +battery_nominal_energy_capacity: 32000 # Wh (32 kWh) +battery_discharge_power_max: 11000 # W +battery_charge_power_max: 11000 # W +battery_discharge_efficiency: 0.96 +battery_charge_efficiency: 0.96 +battery_minimum_state_of_charge: 0.1 # fraction (= 10%) +battery_maximum_state_of_charge: 1.0 # fraction (= 100%) +battery_target_state_of_charge: 0.6 # fraction — end-of-day target + +# ── Deferrable loads ────────────────────────────────────────────────────────── +# Add EV here when OpenEVSE is integrated +number_of_deferrable_loads: 0 +nominal_power_of_deferrable_loads: [] +operating_hours_of_each_deferrable_load: [] +treat_deferrable_load_as_semi_cont: [] +set_deferrable_load_single_constant: [] +start_timesteps_of_each_deferrable_load: [] +end_timesteps_of_each_deferrable_load: [] diff --git a/dashboard_card.yaml b/dashboard_card.yaml new file mode 100644 index 0000000..a1f4888 --- /dev/null +++ b/dashboard_card.yaml @@ -0,0 +1,87 @@ +# EMHASS Dashboard Card +# Add this to your dashboard (Dashboard → Edit → Add Card → Manual) + +type: entities +title: EMHASS Energy Status +show_header_toggle: false +entities: + # Current Status + - type: section + label: Current Status + - entity: sensor.current_hour_price + name: Current Price + icon: mdi:cash + secondary_info: "{{ state_attr('sensor.nordpool_kwh_lv_eur_3_10_021', 'low_price') }}" + - entity: sensor.battery_power_state + name: Battery State + icon: > + {% if is_state('sensor.battery_power_state', 'charging') %}mdi:battery-charging + {% elif is_state('sensor.battery_power_state', 'discharging') %}mdi:battery-minus + {% else %}mdi:battery{% endif %} + - entity: sensor.deye_inverter_mqtt_battery_soc + name: Battery SOC + icon: mdi:battery-{{ (states('sensor.deye_inverter_mqtt_battery_soc') | float(0) / 10) | round(0) | int }}0 + - entity: sensor.deye_inverter_mqtt_battery_power + name: Battery Power + secondary_info: "Target: {{ states('sensor.soc_batt_forecast') }}%" + + # Forecast Comparison + - type: section + label: Forecast vs Reality + - entity: sensor.battery_soc_forecast_error + name: SOC Forecast Error + icon: > + {% set err = states('sensor.battery_soc_forecast_error') | float(0) %} + {% if err | abs < 5 %}mdi:check-circle + {% elif err | abs < 10 %}mdi:alert-circle + {% else %}mdi:alert-octagon{% endif %} + - entity: sensor.soc_batt_forecast + name: Target SOC (Forecast) + icon: mdi:target + + # Daily Totals + - type: section + label: Today + - entity: sensor.deye_inverter_mqtt_daily_production + name: PV Production + icon: mdi:solar-power + - entity: sensor.deye_inverter_mqtt_daily_energy_bought + name: Grid Import + icon: mdi:transmission-tower-import + - entity: sensor.deye_inverter_mqtt_daily_energy_sold + name: Grid Export + icon: mdi:transmission-tower-export + - entity: sensor.deye_inverter_mqtt_daily_battery_charge + name: Battery Charged + icon: mdi:battery-charging + - entity: sensor.deye_inverter_mqtt_daily_battery_discharge + name: Battery Discharged + icon: mdi:battery-minus + - entity: sensor.deye_inverter_mqtt_daily_load_consumption + name: Load Consumption + icon: mdi:home-lightning-bolt + +--- +# Alternative: Compact History Graph +type: history-graph +title: Battery SOC: Actual vs Forecast +entities: + - entity: sensor.deye_inverter_mqtt_battery_soc + name: Actual + color: "#4CAF50" + - entity: sensor.soc_batt_forecast + name: Forecast + color: "#FF9800" +hours_to_show: 48 +refresh_interval: 300 + +--- +# Price Timeline Card +type: entities +title: Price Timeline +entities: + - type: custom:timeline-card + entities: + - entity: sensor.nordpool_kwh_lv_eur_3_10_021 + name: NordPool Price + title: Spot Price Timeline \ No newline at end of file diff --git a/migration_log.md b/migration_log.md new file mode 100644 index 0000000..757c81c --- /dev/null +++ b/migration_log.md @@ -0,0 +1,76 @@ +# Migration Log — HA + Domoticz + +## 2026-04-06 — EMHASS Initial Configuration + +### Discovery completed +- All critical Deye inverter sensors confirmed working via MQTT +- Sign conventions verified: + - `battery_power`: positive = discharging, negative = charging + - `total_grid_power`: positive = import, negative = export + - Formula: `grid + pv = load + (-battery)` ✓ + +### Sensor decisions +| Role | Sensor chosen | Notes | +|------|--------------|-------| +| PV power | `sensor.deye_inverter_mqtt_dc_total_power` | DC total (PV1+PV2) | +| House load | `sensor.load_power_no_var_loads` | Template sensor, ready for EV subtraction later | +| Grid power | `sensor.deye_inverter_mqtt_total_grid_power` | Signed, +import/-export | +| Battery SOC | `sensor.deye_inverter_mqtt_battery_soc` | Direct from inverter | +| Battery power | `sensor.deye_inverter_mqtt_battery_power` | Signed, matches EMHASS convention | +| Spot price | `sensor.nordpool_kwh_lv_eur_3_10_021` | LV zone, 15-min resolution, has today/tomorrow arrays | + +### Legacy sensors to clean up (not used in EMHASS) +- `sensor.deye_pv1_power`, `sensor.deye_pv1_voltage`, `sensor.deye_pv1_current` +- `sensor.deye_pv2_power`, `sensor.deye_pv2_voltage`, `sensor.deye_pv2_current` +- `sensor.deye_total_pv_power`, `sensor.deye_pv_day_energy`, `sensor.deye_total_pv_energy` +- `sensor.total_ev_power`, `sensor.total_ev_energy` (CT clamp readings, not OpenEVSE integration) +- `sensor.deye` (utility meter, check if still needed) + +### EMHASS config written +- File: `config_emhass.yaml` in this repo +- Deploy to HA at: `/config/emhass/config_emhass.yaml` +- Version targeted: v0.17.1 +- Mode: self-consumption, no grid export, no EV (to be added later) +- Key parameters: 32 kWh battery, 11 kW charge/discharge, 16090 W grid limit, 10% min SOC + +### MCP setup +- Fixed hass-mcp env var (was `HATOKEN`, needed `HA_TOKEN`) +- Installed `uv`/`uvx` to `~/.local/bin` (required by hass-mcp) +- Config stored in `~/.claude.json` (local scope, this project) + +### Completed — 2026-04-06 + +#### Config deployment +- Deployed `config.json` to `/addon_configs/5b918bf2_emhass/config.json` +- Had to find correct path (newer HA OS uses `/addon_configs/{slug}/` not `/homeassistant/`) +- Fixed sensor: switched to `sensor.deye_inverter_mqtt_total_load_power` (has history, unlike `load_power_no_var_loads`) + +#### NordPool integration +- EMHASS v0.17 has no native NordPool sensor support +- Solution: `load_cost_forecast_method: "list"` + pass price array at runtime +- Automation builds 48-value list from NordPool `tomorrow` attribute (resampled from 15-min to 30-min) +- Fixed cost added: 0.1437 €/kWh (distribution + VAT) + +#### Files updated on HA +- `/addon_configs/5b918bf2_emhass/config.json` — EMHASS config +- `/homeassistant/configuration.yaml` — added `rest_command` for EMHASS API calls +- `/homeassistant/automations.yaml` — added `emhass_dayahead_optimization` automation + +#### Daily automation +- Triggers at 14:30 EET/EEST +- Waits up to 60min for `tomorrow_valid == true` +- Resamples NordPool prices, adds fixed cost, POSTs to EMHASS +- Runs `dayahead-optim` → `publish-data` + +#### Verified working +- `sensor.p_pv_forecast` — PV forecast (W) +- `sensor.p_load_forecast` — Load forecast (W) +- `sensor.p_grid_forecast` — Grid import forecast (W) +- `sensor.soc_batt_forecast` — Target end-of-day SOC (%) +- `sensor.unit_load_cost` — First-hour price from NordPool + +### Remaining / Future + +1. **Switch load sensor** — after `sensor.load_power_no_var_loads` accumulates 24h+ history, switch back for more accurate EV separation +2. **OpenEVSE integration** — when charger is connected, add `number_of_deferrable_loads: 1` for smart EV charging +3. **Legacy sensor cleanup** — remove unused `sensor.deye_*` duplicates from old configuration