Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Marstek Cascade Automation (Modbus)
|
||||
|
||||
HA automation `automation.marstek_modbus_steuerung` ("Marstek Kaskaden-Steuerung (Modbus)") controls the Marstek Venus battery as an overflow/reserve storage, capped at 2500W. It runs every 2 minutes and decides whether to charge, discharge, or stop the Marstek based on SMA battery state and home power balance.
|
||||
|
||||
## Automation Structure
|
||||
|
||||
Retrieved via REST API: `GET /api/config/automation/config/{numeric_id}` (numeric ID from automation entity `attributes.id` field — in this case `1773314167598`).
|
||||
|
||||
```yaml
|
||||
alias: Marstek Kaskaden-Steuerung (Modbus)
|
||||
description: "Marstek arbeitet als reiner Überlauf/Reserve-Speicher, gedeckelt auf max. 2500 W."
|
||||
mode: single
|
||||
triggers:
|
||||
- minutes: "/2"
|
||||
trigger: time_pattern
|
||||
actions:
|
||||
- variables:
|
||||
production: "{{ states('sensor.sn_3007105790_metering_power_supplied') | float(0) }}"
|
||||
power_home: "{{ states('sensor.sn_3007105790_metering_power_absorbed') | float(0) }}"
|
||||
soc: "{{ states('sensor.sn_3007105790_battery_soc_total') | float(0) }}"
|
||||
battery_charge: "{{ states('sensor.sn_3007105790_battery_power_charge_total') | float(0) }}"
|
||||
discharge: "{{ states('sensor.sn_3007105790_battery_power_discharge_total') | float(0) }}"
|
||||
marstek_current_power: "{{ states('number.marstek_venus_modbus_ladeleistung_einstellen') | float(0) }}"
|
||||
marstek_current_discharge: "{{ states('number.marstek_venus_modbus_entladeleistung_einstellen') | float(0) }}"
|
||||
echter_ueberschuss: "{{ production - power_home + marstek_current_power - marstek_current_discharge }}"
|
||||
echter_bedarf: "{{ power_home - production + marstek_current_discharge - marstek_current_power }}"
|
||||
- choose:
|
||||
# Option 1: Charge when SMA battery is full AND there's surplus
|
||||
- conditions:
|
||||
- "{{ (soc >= 80 or battery_charge >= 3900) and echter_ueberschuss > 50 }}"
|
||||
sequence:
|
||||
- action: number.set_value
|
||||
target:
|
||||
entity_id: number.marstek_venus_modbus_ladeleistung_einstellen
|
||||
data:
|
||||
value: "{{ [ echter_ueberschuss | int, 2500 ] | min }}"
|
||||
- action: select.select_option
|
||||
target:
|
||||
entity_id: select.marstek_venus_modbus_force_mode
|
||||
data:
|
||||
option: charge
|
||||
# Option 2: Discharge when SMA battery is low AND there's demand
|
||||
- conditions:
|
||||
- "{{ (soc <= 25 or discharge >= 3900) and echter_bedarf > 50 }}"
|
||||
sequence:
|
||||
- action: number.set_value
|
||||
target:
|
||||
entity_id: number.marstek_venus_modbus_entladeleistung_einstellen
|
||||
data:
|
||||
value: "{{ [ echter_bedarf | int, 2500 ] | min }}"
|
||||
- action: select.select_option
|
||||
target:
|
||||
entity_id: select.marstek_venus_modbus_force_mode
|
||||
data:
|
||||
option: discharge
|
||||
# Default: stop
|
||||
default:
|
||||
- action: select.select_option
|
||||
target:
|
||||
entity_id: select.marstek_venus_modbus_force_mode
|
||||
data:
|
||||
option: stop
|
||||
- action: number.set_value
|
||||
target:
|
||||
entity_id: number.marstek_venus_modbus_ladeleistung_einstellen
|
||||
data:
|
||||
value: 0
|
||||
- action: number.set_value
|
||||
target:
|
||||
entity_id: number.marstek_venus_modbus_entladeleistung_einstellen
|
||||
data:
|
||||
value: 0
|
||||
```
|
||||
|
||||
## Marstek Modbus Control Entities
|
||||
|
||||
| Entity | Type | Purpose |
|
||||
|--------|------|---------|
|
||||
| `number.marstek_venus_modbus_ladeleistung_einstellen` | number | Charge power setpoint (W) |
|
||||
| `number.marstek_venus_modbus_entladeleistung_einstellen` | number | Discharge power setpoint (W) |
|
||||
| `number.marstek_venus_modbus_maximale_ladeleistung` | number | Max charge power (hardware limit, 2500W) |
|
||||
| `number.marstek_venus_modbus_maximale_entladeleistung` | number | Max discharge power (hardware limit, 2500W) |
|
||||
| `select.marstek_venus_modbus_force_mode` | select | Force mode: `charge` / `discharge` / `stop` |
|
||||
| `select.marstek_venus_modbus_modus` | select | User mode: `anti_feed` (prevents grid export) |
|
||||
| `switch.marstek_venus_modbus_rs485_steuermodus` | switch | RS485 control mode (must be ON for Modbus control) |
|
||||
|
||||
Measured values (read-only):
|
||||
| Entity | Purpose |
|
||||
|--------|---------|
|
||||
| `sensor.marstek_venus_modbus_ac_leistung` | Actual AC output power (W) |
|
||||
| `sensor.marstek_venus_modbus_batterieleistung` | Battery power (W, negative = discharging) |
|
||||
| `sensor.marstek_venus_modbus_batterie_ladezustand` | SoC (%) |
|
||||
| `sensor.marstek_venus_modbus_entladeleistung` | Actual discharge power (W) |
|
||||
| `sensor.marstek_venus_modbus_ladeleistung` | Actual charge power (W) |
|
||||
|
||||
## The Feedback-Loop Bug (Root Cause of ~200W Grid Import)
|
||||
|
||||
### The Problem
|
||||
|
||||
The `echter_bedarf` formula uses **setpoint** values (`number.*_einstellen`) instead of **measured** values:
|
||||
|
||||
```yaml
|
||||
echter_bedarf: "{{ power_home - production + marstek_current_discharge - marstek_current_power }}"
|
||||
```
|
||||
|
||||
Where:
|
||||
- `marstek_current_discharge` = `states('number.marstek_venus_modbus_entladeleistung_einstellen')` — the **setpoint**, not actual discharge
|
||||
- `marstek_current_power` = `states('number.marstek_venus_modbus_ladeleistung_einstellen')` — the **charge setpoint**, not actual charge
|
||||
|
||||
### Why This Spirals
|
||||
|
||||
With `power_home=870W`, `production=0W` (nighttime), and starting setpoint=0:
|
||||
|
||||
| Cycle | Setpoint | echter_bedarf = 870 + setpoint | New Setpoint = min(bedarf, 2500) |
|
||||
|-------|----------|--------------------------------|----------------------------------|
|
||||
| 1 | 0 | 870 | 870 |
|
||||
| 2 | 870 | 1740 | 1740 |
|
||||
| 3 | 1740 | 2610 | 2500 (capped) |
|
||||
|
||||
The controller ramps to 2500W within 3 cycles (6 minutes). The Marstek's `anti_feed` mode then throttles actual output to ~650W (preventing grid export). The mismatch between the 2500W command and the ~650W actual output leaves ~200W on the grid.
|
||||
|
||||
### The Fix (Applied 2026-07-10)
|
||||
|
||||
The user requested consistency with the existing automation structure. Rather than introducing new variables or rewriting the formulas, the fix swaps the **underlying sensor entities** from setpoints to measured values, keeping all variable names and formula structures unchanged:
|
||||
|
||||
```yaml
|
||||
# BROKEN (uses setpoints → feedback loop):
|
||||
marstek_current_power: "{{ states('number.marstek_venus_modbus_ladeleistung_einstellen') | float(0) }}"
|
||||
marstek_current_discharge: "{{ states('number.marstek_venus_modbus_entladeleistung_einstellen') | float(0) }}"
|
||||
|
||||
# FIXED (uses measured values → stable, same variable names, same formulas):
|
||||
marstek_current_power: "{{ states('sensor.marstek_ladeleistung') | float(0) }}"
|
||||
marstek_current_discharge: "{{ states('sensor.marstek_entladeleistung') | float(0) }}"
|
||||
```
|
||||
|
||||
The `echter_ueberschuss` and `echter_bedarf` formulas were NOT changed — only the two variable definitions above. This is the minimal, consistent patch.
|
||||
|
||||
With the fix: `echter_bedarf = 870 - 0 + 650 - 0 = 1520` → but `marstek_current_discharge` is now the *measured* 650W, not the setpoint. Next cycle: the Marstek adjusts toward the new setpoint, `marstek_current_discharge` tracks the actual output, and `echter_bedarf` converges to the true residual (~220W). No positive feedback, no spiral to 2500W.
|
||||
|
||||
**How the fix was applied:** Retrieved the automation config via REST API (`GET /api/config/automation/config/{id}`), modified the two variable values in the JSON, and POSTed it back. The HA REST API accepts the full config object as the request body and returns `{"result":"ok"}`.
|
||||
|
||||
```javascript
|
||||
// In browser_console (after HA login):
|
||||
(async () => {
|
||||
const conn = await window.hassConnection;
|
||||
const token = conn.auth.data.access_token;
|
||||
const resp = await fetch('/api/config/automation/config/1773314167598', {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
const config = await resp.json();
|
||||
// Swap setpoint sensors for measured sensors
|
||||
const vars = config.actions[0].variables;
|
||||
vars.marstek_current_power = "{{ states('sensor.marstek_ladeleistung') | float(0) }}";
|
||||
vars.marstek_current_discharge = "{{ states('sensor.marstek_entladeleistung') | float(0) }}";
|
||||
// POST back
|
||||
const updateResp = await fetch('/api/config/automation/config/1773314167598', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
return await updateResp.text(); // {"result":"ok"}
|
||||
})()
|
||||
```
|
||||
|
||||
### General Lesson: Setpoint vs Measured in Control Automations
|
||||
|
||||
**Never use a setpoint (command/output) variable in a feedback formula that computes the next setpoint.** This creates positive feedback — the controller amplifies its own output every cycle. Always use the **measured process variable** (sensor reading) to compute the error/demand. This is classic control theory: the feedback path must measure the actual plant output, not the controller's commanded output.
|
||||
|
||||
Pattern to watch for in HA automations:
|
||||
```yaml
|
||||
# DANGER: variable reads from number.*_einstellen (setpoint) → feedback loop
|
||||
marstek_current_X: "{{ states('number.marstek_venus_modbus_X_einstellen') | float(0) }}"
|
||||
|
||||
# SAFE: variable reads from sensor.* (measured) → stable control
|
||||
marstek_current_X: "{{ states('sensor.marstek_venus_modbus_X') | float(0) }}"
|
||||
```
|
||||
|
||||
## Interaction with Marstek `anti_feed` Mode
|
||||
|
||||
The Marstek's user mode is set to `anti_feed` (`select.marstek_venus_modbus_modus`), which prevents exporting power to the grid. When the automation commands 2500W discharge but home only needs 870W, the Marstek internally throttles to avoid feeding 1630W back to the grid. This is correct behavior by the Marstek — the bug is in the automation commanding too much, not in the Marstek refusing to deliver.
|
||||
|
||||
## How to Retrieve HA Automation YAML
|
||||
|
||||
```javascript
|
||||
// In browser_console (after HA login):
|
||||
(async () => {
|
||||
const conn = await window.hassConnection;
|
||||
const token = conn.auth.data.access_token;
|
||||
// Get numeric ID from entity attributes
|
||||
const stateResp = await fetch('/api/states/automation.ENTITY_ID', {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
const state = await stateResp.json();
|
||||
const numericId = state.attributes.id;
|
||||
// Fetch full automation config
|
||||
const configResp = await fetch(`/api/config/automation/config/${numericId}`, {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
const config = await configResp.json();
|
||||
return JSON.stringify(config);
|
||||
})()
|
||||
```
|
||||
|
||||
Or via REST API directly (no browser needed if token available):
|
||||
```bash
|
||||
# Step 1: Get numeric ID
|
||||
NUMERIC_ID=$(curl -s "$HA_URL/api/states/automation.ENTITY_ID" \
|
||||
-H "Authorization: Bearer $HA_TOKEN" | python3 -c "import sys,json; print(json.load(sys.stdin)['attributes']['id'])")
|
||||
# Step 2: Get config
|
||||
curl -s "$HA_URL/api/config/automation/config/$NUMERIC_ID" \
|
||||
-H "Authorization: Bearer $HA_TOKEN" | python3 -m json.tool
|
||||
```
|
||||
Reference in New Issue
Block a user