# HA Energy/Power Analysis Methodology

How to diagnose unexpected power consumption (e.g. nighttime grid draw) using HA history data. Based on a session diagnosing ~200W nighttime grid import caused by a heat pump water heating cycle.

## Sensor Inventory (This Homelab)

### Grid / Net Power
| Entity | Name | Notes |
|--------|------|-------|
| `sensor.evcc_grid_power` | Leistung Netz | EVCC aggregate grid power (W). Positive = import. |
| `sensor.netzfluss_saldo` | Tibber Pulse Netzfluss Saldo | Tibber Pulse grid balance (W). Negative = export. Higher resolution than EVCC. |
| `sensor.stromverbrauch_haus_netto` | Stromverbrauch Haus netto | Net house power consumption (W) |

### Home / Load
| Entity | Name | Notes |
|--------|------|-------|
| `sensor.evcc_home_power` | Leistung Heim | Total home consumption (W) |
| `sensor.stromverbrauch_rest` | Stromverbrauch "Rest" | Unknown/rest consumption |

### PV
| Entity | Name | Notes |
|--------|------|-------|
| `sensor.evcc_pv_power` | Leistung PV | Aggregate PV power (W) |
| `sensor.hm_1500_power` | HM-1500 Power | Hoymiles HM-1500 via OpenDTU/MQTT |
| `sensor.schuppen_string_1_power` | Schuppen 1 Power | Hoymiles Schuppen 1 |
| `sensor.schuppen_2_power` | Schuppen 2 Power | Hoymilles Schuppen 2 |

### Batteries
| Entity | Name | Notes |
|--------|------|-------|
| `sensor.evcc_battery_power` | Leistung Batterie | EVCC aggregate battery (W). Positive = discharging. |
| `sensor.evcc_battery_soc` | Batterie Ladezustand | EVCC aggregate SOC (%) |
| `sensor.sn_3007105790_battery_power_discharge_total` | SMA Battery Discharge | SMA SunnyBoyStorage discharge (W) |
| `sensor.sn_3007105790_battery_power_charge_total` | SMA Battery Charge | SMA SunnyBoyStorage charge (W) |
| `sensor.sn_3007105790_battery_soc_total` | SMA Battery SOC | SMA SunnyBoyStorage SOC (%) |
| `sensor.marstek_venus_modbus_batterieleistung` | Marstek Batterieleistung | Marstek Venus (W). Negative = discharging. |
| `sensor.marstek_venus_modbus_ac_leistung` | Marstek AC-Leistung | Marstek AC output (W) |
| `sensor.marstek_venus_modbus_batterie_ladezustand` | Marstek SoC | Marstek Venus SOC (%) |
| `sensor.marstek_target_power` | Marstek Target Power | EVCC/setpoint target for Marstek (W) |
| `sensor.sma_target_power` | SMA Target Power | EVCC/setpoint target for SMA (W) |

### Large Consumers (Individual)
| Entity | Name | Notes |
|--------|------|-------|
| `sensor.shellypro3em_08f9e0e98b9c_total_active_power` | Wärmepumpe Total | Heat pump total active power (W). **Major consumer — up to 5.4kW.** |
| `sensor.strommessung_sauna_total_active_power` | Sauna Total | Sauna total active power (W) |
| `sensor.cu401b_s_verdichterleistung` | CU401B Verdichterleistung | Heat pump compressor power (kW) |
| `sensor.cu401b_s_verdichtermodulation` | CU401B Verdichtermodulation | Heat pump compressor modulation (%) |
| `sensor.cu401b_s_kompressorphase` | CU401B Kompressorphase | Compressor phase: off/preparing/heating/pause |
| `sensor.cu401b_s_ww_speichertemperatur` | WW-Speichertemperatur | Hot water tank temp (°C). Target: 46°C, Hysteresis: 4K |
| `sensor.ev_charger_schon_leistung_ladestation` | EV Charger Leistung | SMA EV Charger station power (W) |
| `sensor.evcc_wallbox_charge_power` | Wallbox Ladeleistung | EVCC wallbox charge power (kW) |
| `sensor.trockner_device_power` | Trockner Power | Dryer power (W) |
| `sensor.samsung_qn90aa_65_tv_power` | Samsung TV Power | TV power (W) |

### EVCC Config / Tariff
| Entity | Name | Notes |
|--------|------|-------|
| `number.evcc_residual_power` | Restleistung | EVCC residual power target (W). Default 100W. |
| `sensor.evcc_tariff_grid` | Kosten Netz | Current grid price (€/kWh) |
| `binary_sensor.evcc_battery_grid_charge_active` | Hausbatterie Netzladen | Whether battery is charging from grid |

### Heat Pump Config (ViCare / CU401B S)
| Entity | Name | Notes |
|--------|------|-------|
| `climate.vicare_heating` | CU401B S Heizung | Climate entity. Presets: comfort/eco/home/sleep |
| `number.cu401b_s_ww_temperatur` | WW-Temperatur | WW target temp (°C). Currently 46. |
| `number.cu401b_s_ww_hystereseschalter_ein` | WW-Hysterese ein | WW hysteresis ON (K). Currently 4. |
| `number.cu401b_s_ww_hystereseschalter_aus` | WW-Hysterese aus | WW hysteresis OFF (K). Currently 4. |
| `automation.komfort_heizung` | Komfort Heizung | Automation controlling heating comfort mode |

## Analysis Workflow

1. **Confirm the anomaly**: Query `sensor.evcc_grid_power` history for the suspected time window. Summarize in 30-min buckets to see the pattern. **Important**: Ask the user for approximate time — they may point to a different window than the first obvious spike. In one session, the first analysis found a 02:47 heat pump spike, but the user clarified the issue was at 04:30+, revealing a second smaller spike that was initially overlooked.

2. **Decompose the power equation**: At any moment: `grid = home - pv - battery_discharge + battery_charge`. Query all four simultaneously to see which component is causing the imbalance.

3. **Check battery SOC trajectory**: If batteries drain to low SOC during the night, they can't cover the remaining demand → grid import kicks in. Plot SOC over time for both batteries.

4. **Identify the triggering consumer**: Query individual large-consumer sensors (heat pump, sauna, EV charger, dryer) for the same time window. Look for spikes or sustained elevated consumption.

5. **Correlate with heat pump phases**: The `cu401b_s_kompressorphase` and `cu401b_s_verdichtermodulation` entities show exactly when the compressor starts, what phase it's in, and how hard it's working. The `ww_speichertemperatur` shows whether it's a hot-water heating cycle (temp rising toward target).

6. **Check EVCC control targets**: `marstek_target_power` and `sma_target_power` show what EVCC is commanding the batteries to do. If these are 0, EVCC isn't actively discharging the batteries — they may be in passive mode.

7. **Summarize findings**: Present a timeline table showing the key events and a root-cause explanation, followed by prevention options.

### Multiple Spikes Per Night (2026-07-09 Session)

In one session, two distinct grid import spikes were found in a single night:

| Time | Grid Peak | Home Peak | Heat Pump | Cause |
|------|-----------|-----------|-----------|-------|
| 02:47 | 4,808 W | 6,151 W | 5,440 W | Heat pump WW heating (compressor 47-50% modulation, WW temp 38→47°C) |
| 04:30 | 3,570 W | 4,920 W | 91 W (standby) | **Unknown** — heat pump was idle. Possibly a second WW cycle or another consumer not covered by monitored sensors. |

**Lesson**: Don't assume the first spike is the only one. Query the full 02:00–07:00 window and look for ALL grid import periods >100W. The 04:30 spike showed home power at 4,920W but the heat pump was only drawing 91W — the 4,800W delta came from an unmonitored consumer. Future analysis should query ALL available power sensors (including `sensor.stromverbrauch_haus_netto` which aggregates differently from `evcc_home_power`) and consider unmonitored circuits.

## Prevention Strategies for Nighttime Grid Import

### Heat Pump WW Timing
The most impactful fix: shift hot water preparation to daytime PV hours.

- **ViCare schedule**: Set WW heating to only occur between 10:00–15:00 (main PV production window).
- **Increase hysteresis**: Change `ww_hystereseschalter_ein` from 4K to 6K → starts later, fewer cycles.
- **Lower WW target**: Reduce `ww_temperatur` from 46°C to 43°C → less energy per cycle.

### Battery SOC Management
Ensure batteries start the night at 100%:

- **EVCC priority SOC**: Ensure `evcc_priority_soc` is set appropriately so batteries reserve enough for overnight.
- **Force charge before sunset**: If PV forecast predicts low production, trigger battery charging from PV surplus in late afternoon.
- **Marstek discharge limit**: `number.marstek_venus_modbus_entladeleistung_einstellen` controls max discharge rate. Lowering it extends battery runtime but may not fully cover peaks.

### Combination Approach (Recommended)
Both WW timing AND battery management together provide the most robust solution.

## EVCC-Side Battery Discharge Diagnosis

When the question shifts from "what consumed the power?" to "why didn't the battery cover the gap?", the EVCC `api/state` endpoint provides the answer. This is a different diagnostic axis from the consumption-side analysis above.

### Key Fields in `curl -s http://10.0.30.10:7070/api/state`

| Field | Path | Meaning |
|-------|------|---------|
| Controllable | `battery.devices[].controllable` | `false` = EVCC reads SoC/power but cannot command discharge |
| Discharge control | `batteryDischargeControl` | `false` = passive monitoring, no active discharge management |
| Residual power | `residualPower` | Watts EVCC tolerates from grid before reacting (default 100) |
| Battery mode | `batteryMode` | `unknown` = no active mode set |
| Per-battery power | `battery.devices[].power` | Current output (W) — compare to home power |
| Per-battery SOC | `battery.devices[].soc` | High SOC + low output = battery is limited, not empty |

### Diagnosis Flow

1. **Check `controllable`** — if `false`, EVCC cannot increase discharge. The battery's own internal limit is the bottleneck.
2. **Check `residualPower`** — if >0, EVCC deliberately allows that much grid import as baseline.
3. **Compare battery output to home power** — if battery output << home power and SOC is high, the battery's internal discharge limit (not EVCC) is the constraint.
4. **Verify by checking spike behavior** — if the battery briefly delivered much more during a spike (e.g. Marstek hit 1,437W during heat pump cycle but settled to ~650W steady-state), that confirms an internal limiter, not a capacity issue.

### Case Study: Marstek Venus Not Covering 200W Gap (2026-07-10)

**Symptom**: ~200W continuous grid import 03:00–04:00 despite Marstek SoC at 73%.

**Initial hypothesis (INCOMPLETE)**: EVCC `controllable: false` + `residualPower: 100` + Marstek internal discharge limit ≈ 650W.

**Actual root cause**: The HA automation "Marstek Kaskaden-Steuerung (Modbus)" (`automation.marstek_modbus_steuerung`) controls the Marstek, NOT EVCC. EVCC sees `controllable: false` because it's only a passive observer. The automation has a **feedback-loop bug** in its `echter_bedarf` formula:

```yaml
# BUG: uses setpoint (number.*_einstellen) instead of measured (sensor.*)
echter_bedarf: "{{ power_home - production + marstek_current_discharge - marstek_current_power }}"
# marstek_current_discharge = states('number.marstek_venus_modbus_entladeleistung_einstellen')  ← SETPOINT
```

This causes the discharge setpoint to spiral: 0 → 870 → 1740 → 2500 (capped) within 3 cycles (6 min). The Marstek's `anti_feed` mode then throttles actual output to ~650W (preventing grid export from the oversized command). The ~200W gap is the difference between what the automation commands (2500W) and what the Marstek actually delivers after anti_feed throttling (~650W), versus what the house needs (~870W).

**Evidence**: During the 02:43–02:57 heat pump spike, Marstek delivered up to 1,437W (proving capacity exists), but settled back to ~650W afterward — consistent with the automation ramping the setpoint up and anti_feed throttling back.

**Fix**: Replace setpoint variables with measured AC power in the automation:
```yaml
# FIXED:
marstek_actual_ac: "{{ states('sensor.marstek_venus_modbus_ac_leistung') | float(0) }}"
echter_bedarf: "{{ power_home - production - marstek_actual_ac }}"
```
Result: `echter_bedarf = 870 - 0 - 650 = 220W` → new setpoint = 650 + 220 = 870W. Stable convergence.

See `references/marstek-cascade-automation.md` for the full automation YAML and analysis.

## Query Templates

See `references/ha-browser-console-access.md` for the browser-console technique used to execute these queries when no local API token is available.

### Summary query (all sensors, 30-min buckets)
```javascript
// Adapt entity list and time range as needed
const entities = 'sensor.evcc_grid_power,sensor.evcc_home_power,sensor.evcc_battery_power,sensor.evcc_pv_power';
const url = '/api/history/period/2026-07-09T03:00:00?filter_entity_id=' + entities +
            '&end_time=2026-07-09T06:00:00&minimal_response';
```

### Individual consumer spike detection
```javascript
// Filter history records where power > threshold
const spikes = hist.filter(h => parseFloat(h.state) > 100).map(h => ({
  t: h.last_changed.substring(11, 16),
  v: parseFloat(h.state)
}));
```
