---
name: midea-ac-ha
description: Midea Porta Split Klimaanlage in Home Assistant 2026 einbinden — Integration, Patches, Token-Beschaffung und Diagnose.
version: 1.0.1
author: Claude (via Hermes, 2026-06-04)
license: MIT
metadata:
  hermes:
    tags: [home-assistant, midea, climate, hvac, smart-home]
    related_skills: [home-assistant, zimaos-administration]
    source: Claude Code handover 2026-06-04
---

# Midea Klimaanlage in Home Assistant 2026

Einbindung einer Midea Porta Split Klimaanlage in Home Assistant 2026.5.4 auf ZimaOS Docker.

## Schnelldiagnose: „Klima geht nicht" — strukturierter Flow

Wenn der User meldet, die Klimaanlage funktioniere nicht, diesen Flow abarbeiten (jeder Schritt <15s, parallel wo möglich):

1. **HA Container läuft?** `docker ps --filter name=homeassistant`
2. **AC Entity Status abfragen** (Inline-Python mit Token-Holing, siehe Steuerung unten) → `state`, `realtime_power`, `current_temperature`
3. **Integration geladen?** Config-Entry prüfen: `domain=midea_ac_lan`, `disabled_by` leer, `protocol=3`
4. **Patches intakt?** Drei Greps im Container: `UnitOfTemperature.CELSIUS` in midea_devices.py, `AUX_HEAT` NICHT in climate.py, `async_forward_entry_setups` in __init__.py
5. **Gerät erreichbar?** `timeout 2 bash -c "echo >/dev/tcp/<IP>/6444"` — Port muss offen sein
6. **Steuerung testen:** `set_hvac_mode cool`, 3s warten, Status prüfen. Wenn `state=off` nach erstem Versuch → Retry (bekannter Workaround, siehe Pitfalls)

**Reihenfolge ist wichtig:** Erst Status lesen, dann Integration prüfen, dann Netzwerk, dann Steuerung testen. Nicht blind Befehle feuern.

## Integration

**georgezhao2010/midea_ac_lan** (GitHub, master branch). Download über lokalen Mac + SCP — git auf ZimaOS funktioniert nicht.

```bash
curl -L "https://github.com/georgezhao2010/midea_ac_lan/archive/refs/heads/master.zip" -o /tmp/midea_ac_lan.zip
cd /tmp && unzip -q midea_ac_lan.zip
scp -i ~/.ssh/id_ed25519 -r /tmp/midea_ac_lan-master/custom_components/midea_ac_lan \
  root@100.113.239.44:/DATA/AppData/homeassistant/config/custom_components/
```

## 4 Pflicht-Patches für HA 2026

**Diese Patches MÜSSEN vor dem HA-Start angewendet werden.**

### A) midea_devices.py — Veraltete Einheits-Konstanten ersetzen

```python
from homeassistant.const import (
    Platform, PERCENTAGE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
    CONCENTRATION_PARTS_PER_MILLION, UnitOfTime, UnitOfTemperature,
    UnitOfPower, UnitOfVolume, UnitOfEnergy,
)
TIME_DAYS = UnitOfTime.DAYS
TIME_HOURS = UnitOfTime.HOURS
TIME_MINUTES = UnitOfTime.MINUTES
TIME_SECONDS = UnitOfTime.SECONDS
TEMP_CELSIUS = UnitOfTemperature.CELSIUS
POWER_WATT = UnitOfPower.WATT
VOLUME_LITERS = UnitOfVolume.LITERS
ENERGY_KILO_WATT_HOUR = UnitOfEnergy.KILO_WATT_HOUR
```

### B) climate.py + water_heater.py — TEMP_CELSIUS → UnitOfTemperature

```python
from homeassistant.const import UnitOfTemperature
# Alle return TEMP_CELSIUS ersetzen durch:
return UnitOfTemperature.CELSIUS
```

### C) climate.py — AUX_HEAT entfernen

`ClimateEntityFeature.AUX_HEAT` komplett aus den Feature-Flags entfernen. **Achtung:** Kein `| \ ` am Zeilenende stehen lassen (SyntaxError!).

### D) __init__.py — Config Entry API umbenannt

```python
# ALT (funktioniert nicht mehr in HA 2026):
for platform in ALL_PLATFORM:
    hass.async_create_task(hass.config_entries.async_forward_entry_setup(config_entry, platform))
for platform in ALL_PLATFORM:
    await hass.config_entries.async_forward_entry_unload(config_entry, platform)

# NEU:
hass.async_create_task(hass.config_entries.async_forward_entry_setups(config_entry, ALL_PLATFORM))
await hass.config_entries.async_unload_platforms(config_entry, ALL_PLATFORM)
```

## Token-Problem — der wichtigste Trick

Die MSmartHome Cloud-API gibt für viele Geräte Fehler **3004: value is illegal**. Der eingebaute Fallback-Token (`key_id=99`) wird vom Gerät mit ERROR abgewiesen.

**Lösung: msmart-ng** — holt den Token direkt aus der UDP-Discovery-Antwort, kein Cloud-Login nötig:

```bash
docker exec homeassistant pip install msmart-ng
docker exec homeassistant python3 -m msmart.cli discover <GERAET_IP>
# Ausgabe enthält direkt: token, key, device_id
```

Token anschließend in beide Dateien eintragen + Protokoll auf 3 setzen:

```python
import json

REAL_TOKEN = '<token aus msmart-ng>'
REAL_KEY   = '<key aus msmart-ng>'
DEVICE_ID  = <device_id>

# Device-Config:
with open(f'/DATA/AppData/homeassistant/config/.storage/midea_ac_lan/{DEVICE_ID}.json', 'r') as f:
    dev = json.load(f)
dev.update({'token': REAL_TOKEN, 'key': REAL_KEY, 'protocol': 3})
with open(f'/DATA/AppData/homeassistant/config/.storage/midea_ac_lan/{DEVICE_ID}.json', 'w') as f:
    json.dump(dev, f, indent=2)

# Config Entry:
with open('/DATA/AppData/homeassistant/config/.storage/core.config_entries', 'r') as f:
    data = json.load(f)
for entry in data['data']['entries']:
    if entry.get('domain') == 'midea_ac_lan':
        entry['data'].update({'token': REAL_TOKEN, 'key': REAL_KEY, 'protocol': 3})
with open('/DATA/AppData/homeassistant/config/.storage/core.config_entries', 'w') as f:
    json.dump(data, f, indent=2)
```

## Diagnose-Schnellreferenz

```bash
# Midea-Gerät im Netz finden (Port 6444):
for ip in 192.168.178.{1..254}; do
  (timeout 0.3 bash -c "echo >/dev/tcp/$ip/6444" 2>/dev/null && echo "Midea: $ip") &
done; wait

# Logs nach Neustart prüfen (--since 5m statt --tail, weil docker logs --tail oft hängt):
docker logs --since 5m homeassistant 2>&1 | grep -iE '(midea|handshak|auth|ERROR)' | grep -v pyhap | tail -30
```

**Wichtig:** `docker logs --tail N` kann auf ZimaOS hängen (Timeout nach 30s). Stattdessen `--since 5m` oder `--since 1h` verwenden — das ist zuverlässig.

## V2 vs V3 Protokoll

V2-Verbindung sieht erfolgreich aus, liefert aber bei V3-Geräten **nie Status-Updates**. **Immer Protokoll 3 + richtigen Token verwenden.**

## Bonus: Celsius + Raum-Zuweisung

```python
# Celsius (in core.config):
config['data']['unit_system_v2'] = 'metric'  # statt 'us_customary'

# Raum (in core.device_registry):
dev['area_id'] = 'schlafzimmer'  # area_id aus core.area_registry
```

## ⚠️ AGENT TRIGGER: Zeitverzögerte Befehle ("in X Minuten ausschalten/einschalten")

**Wenn der User sagt „schalte die Klima in X Minuten aus/ein" oder „nach X Minuten ausschalten", MUSS der Agent SOFORT einen one-shot Cronjob mit `cronjob create` anlegen.** Nicht warten, nicht die AC sofort schalten, nicht den User vertrösten.

**Wrapper-Scripts (liegen unter `/DATA/.hermes/scripts/`):**

| Script | Aktion |
|--------|--------|
| `ac-ein.sh` | Einschalten (Cool) |
| `ac-aus.sh` | Ausschalten |
| `ac-heat.sh` | Heizen |
| `ac-dry.sh` | Entfeuchten |
| `ac-fan.sh` | Nur Lüfter |
| `ac-auto.sh` | Automatik |

```bash
# Beispiel: User sagt "in 30 Minuten ausschalten"
# → SOFORT diesen Cronjob anlegen:
cronjob create \
  schedule="$(date '+%Y-%m-%dT%H:%M:%S%:z' -d '+30 minutes')" \
  script="ac-aus.sh" \
  no_agent=true \
  deliver="origin"
```

**Wichtig:**
- `schedule` MUSS ISO-8601 sein: `date '+%Y-%m-%dT%H:%M:%S%:z' -d '+30 minutes'`
- `script` ist der Wrapper-Name OHNE Argumente (z.B. `ac-aus.sh`, nicht `ha-ac-cronjob.sh off`!)
- `no_agent=true` — das Script läuft direkt, kein LLM nötig
- `deliver="origin"` — Bestätigung kommt zurück in den Chat
- **Nie das Datum raten** — immer `date` mit `-d` verwenden, besonders nach Mitternacht!

**Pattern für den Agenten:**
1. User sagt "in X Minuten ausschalten/einschalten"
2. `date '+%Y-%m-%dT%H:%M:%S%:z' -d '+X minutes'` ausführen
3. `cronjob create` mit dem Timestamp + passendem Wrapper (z.B. `ac-aus.sh` oder `ac-ein.sh`)
4. Dem User bestätigen: „Cronjob angelegt, Klima schaltet um HH:MM aus/ein"

## Steuerung per HA API

Die Midea-Integration unterstützt `climate.turn_on` NICHT — der Service wirft `ServiceNotSupported`. Stattdessen einfach den Mode setzen, das schaltet implizit ein:

```bash
# FALSCH (500 Error):
curl -X POST .../api/services/climate/turn_on -d '{"entity_id": "climate.152832117784908_climate"}'

# RICHTIG — nur set_hvac_mode (kein turn_on, kein set_temperature gleichzeitig):
curl -X POST .../api/services/climate/set_hvac_mode \
  -d '{"entity_id": "climate.152832117784908_climate", "hvac_mode": "cool"}'
# Klima geht automatisch an. Kein Chainen von Services.
```

HVAC modes: `off`, `auto`, `cool`, `dry`, `heat`, `fan_only`.

### Inline-Python für schnelle Steuerung (ohne Cronjob-Script)

Für ad-hoc Ein/Aus/Temp/ECO-Änderungen direkt aus Hermes heraus — ein einzelner `sudo python3 -c "..."` Block, der Token holt + Aktion ausführt + Status verifiziert:

```python
sudo python3 -c "
import json, urllib.request, urllib.parse

# 1. Refresh-Token holen (sudo nötig für auth-Datei!)
with open('/DATA/AppData/homeassistant/config/.storage/auth') as f:
    auth = json.load(f)
access_token = None
for rt in auth['data']['refresh_tokens']:
    if rt['token_type'] == 'normal' and rt.get('last_used_at'):
        data = urllib.parse.urlencode({
            'grant_type': 'refresh_token',
            'refresh_token': rt['token'],
            'client_id': rt.get('client_id', 'http://localhost:8123/'),
        }).encode()
        req = urllib.request.Request('http://localhost:8123/auth/token', data=data)
        with urllib.request.urlopen(req) as resp:
            access_token = json.loads(resp.read())['access_token']
        break

# 2. Status prüfen (optional)
req = urllib.request.Request('http://localhost:8123/api/states/climate.152832117784908_climate')
req.add_header('Authorization', f'Bearer {access_token}')
with urllib.request.urlopen(req) as resp:
    state = json.loads(resp.read())
print(f'Vorher: state={state[\"state\"]}, temp={state[\"attributes\"].get(\"temperature\")}, current={state[\"attributes\"].get(\"current_temperature\")}')

# 3. Aktion(en) — Beispiele:
# Einschalten:
data = json.dumps({'entity_id': 'climate.152832117784908_climate', 'hvac_mode': 'cool'}).encode()
req = urllib.request.Request('http://localhost:8123/api/services/climate/set_hvac_mode', data=data)
req.add_header('Authorization', f'Bearer {access_token}')
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req) as resp:
    print(f'set_hvac_mode cool: {resp.read().decode()}')

# Temperatur:
data = json.dumps({'entity_id': 'climate.152832117784908_climate', 'temperature': 21}).encode()
req = urllib.request.Request('http://localhost:8123/api/services/climate/set_temperature', data=data)
req.add_header('Authorization', f'Bearer {access_token}')
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req) as resp:
    print(f'set_temperature 21: {resp.read().decode()}')

# ECO-Modus:
data = json.dumps({'entity_id': 'climate.152832117784908_climate', 'preset_mode': 'eco'}).encode()
req = urllib.request.Request('http://localhost:8123/api/services/climate/set_preset_mode', data=data)
req.add_header('Authorization', f'Bearer {access_token}')
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req) as resp:
    print(f'set_preset_mode eco: {resp.read().decode()}')

# Ausschalten:
data = json.dumps({'entity_id': 'climate.152832117784908_climate', 'hvac_mode': 'off'}).encode()
req = urllib.request.Request('http://localhost:8123/api/services/climate/set_hvac_mode', data=data)
req.add_header('Authorization', f'Bearer {access_token}')
req.add_header('Content-Type', 'application/json')
with urllib.request.urlopen(req) as resp:
    print(f'set_hvac_mode off: {resp.read().decode()}')

print('Done.')
"
```

**Pattern-Notizen:**
- Token-Holing + Aktion(en) + Verifikation in einem Block — kein Script nötig
- `sudo` ist Pflicht für den Auth-File-Read vom Host
- `set_hvac_mode` returned `[]` (leeres Array) — das ist normal, kein Fehler
- Für zeitgesteuerte Aktionen stattdessen Cronjob + `ha_ac_off.sh` verwenden

### API-Token beschaffen (wenn kein Long-Lived Token da)

Long-Lived Access Tokens aus dem Auth-Store sind SHA256-gehasht und als API-Token unbrauchbar. Stattdessen einen normalen Refresh-Token nehmen und per `/auth/token` austauschen.

**Wichtig:** Die `auth`-Datei gehört Home Assistant und ist für den normalen User nicht lesbar. **Immer `sudo` verwenden**, wenn das Script vom Host (nicht aus dem Docker-Container) läuft.

**Pfad-Hinweis:** Der Skill zeigt Container-Pfade (`/config/.storage/...`). Vom ZimaOS-Host aus ist der tatsächliche Pfad `/DATA/AppData/homeassistant/config/.storage/...`. Je nach Ausführungsumgebung den richtigen Pfad verwenden.

```python
import json, urllib.request, urllib.parse
# Vom Host aus (ZimaOS) — sudo nötig:
with open('/DATA/AppData/homeassistant/config/.storage/auth') as f:
    auth = json.load(f)
# Aus dem Docker-Container heraus:
# with open('/config/.storage/auth') as f:
#     auth = json.load(f)
for rt in auth['data']['refresh_tokens']:
    if rt['token_type'] == 'normal' and rt.get('last_used_at'):
        data = urllib.parse.urlencode({
            'grant_type': 'refresh_token',
            'refresh_token': rt['token'],
            'client_id': rt.get('client_id', 'http://localhost:8123/'),
        }).encode()
        req = urllib.request.Request('http://localhost:8123/auth/token', data=data)
        with urllib.request.urlopen(req) as resp:
            access = json.loads(resp.read())['access_token']
        break
# access ist ein JWT, gültig für 30 Minuten
```

Alle API-Calls dann mit `Authorization: Bearer $access`.

### Status abfragen

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8123/api/states/climate.152832117784908_climate"
# Wichtige Felder: state (off/cool/heat/...), attributes.temperature, attributes.current_temperature, attributes.hvac_modes
```

## ECO-Modus + Fan-Interaktion (wichtig!)

**ECO-Modus überschreibt den Fan-Mode auf Auto.** Das ist Midea-spezifisch — ECO optimiert den Lüfter selbst für Energieeffizienz. Fan-Mode NACH ECO setzen wird ignoriert. Die beiden sind nicht gleichzeitig möglich.

Entweder:
- **ECO priorisieren** → Fan bleibt Auto (vom Gerät gesteuert)
- **Fan-Manual priorisieren** → ECO deaktivieren (`preset_mode: "none"`), dann Fan setzen

```bash
# Fan auf Silent (1%) OHNE ECO:
curl -X POST .../api/services/climate/set_preset_mode \
  -d '{"entity_id": "climate.152832117784908_climate", "preset_mode": "none"}'
curl -X POST .../api/services/climate/set_fan_mode \
  -d '{"entity_id": "climate.152832117784908_climate", "fan_mode": "Silent"}'
```

## Vollständiges Attribut-Inventory (climate.152832117784908_climate)

```bash
# Alle Attribute + verfügbaren Modi abfragen:
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8123/api/states/climate.152832117784908_climate" | python3 -c "
import sys, json
s = json.load(sys.stdin)
a = s['attributes']
print('State:', s['state'])
print('Temperature:', a.get('temperature'))
print('Current temp:', a.get('current_temperature'))
print('HVAC modes:', a.get('hvac_modes'))
print('Fan modes:', a.get('fan_modes'))
print('Preset modes:', a.get('preset_modes'))
print('Fan mode current:', a.get('fan_mode'))
print('Preset mode current:', a.get('preset_mode'))
print('Fan speed:', a.get('fan_speed'))
print('ECO mode:', a.get('eco_mode'))
print('Swing modes:', a.get('swing_modes'))
print('Swing mode:', a.get('swing_mode'))
print('Power:', a.get('power'))
print('Realtime power (W):', a.get('realtime_power'))
print('Total energy (kWh):', a.get('total_energy_consumption'))
print('Outdoor temp:', a.get('outdoor_temperature'))
print('Indoor temp:', a.get('indoor_temperature'))
print('Min/Max temp:', a.get('min_temp'), '/', a.get('max_temp'))
print('Target step:', a.get('target_temp_step'))
"
```

**Bekannte Werte für dieses Gerät:**
- `fan_modes`: Silent (speed=1), Low, Medium, High, Full, Auto (speed=102)
- `preset_modes`: none, comfort, eco, boost, sleep, away
- `swing_modes`: Off, Vertical, Horizontal, Both
- `hvac_modes`: off, auto, cool, dry, heat, fan_only
- `min_temp`: 17, `max_temp`: 30, `target_temp_step`: 0.5
- `realtime_power`: aktuelle Watt (z.B. 143.9W im Cool-Modus)
- `total_energy_consumption`: kWh gesamt (z.B. 3.55)

## Zeitgesteuerte Schaltung per Hermes Cronjob

Ein-/Ausschalten zu bestimmten Uhrzeiten über Hermes one-shot Cronjobs. Das Script holt vor jedem API-Call einen frischen Token (Refresh-Token-Austausch), da Access-Tokens nur 30 Min gültig sind.

### Wiederverwendbares Script

Siehe `scripts/ha-ac-cronjob.sh` — nimmt den gewünschten `hvac_mode` als Argument (`off` oder `cool`).

```bash
# Usage:
~/scripts/ha-ac-cronjob.sh off   # Ausschalten
~/scripts/ha-ac-cronjob.sh cool  # Einschalten (Kühlen)
```

**Wrapper-Scripts für Cronjobs** (liegen unter `/DATA/.hermes/scripts/`, nicht im Skill-Verzeichnis):

| Script | Aktion |
|--------|--------|
| `ac-ein.sh` | Einschalten (Cool) |
| `ac-aus.sh` | Ausschalten |
| `ac-heat.sh` | Heizen |
| `ac-dry.sh` | Entfeuchten |
| `ac-fan.sh` | Nur Lüfter |
| `ac-auto.sh` | Automatik |

Jeder Wrapper ist ein 2-Zeiler: `#!/bin/bash\nexec /DATA/.hermes/scripts/ha-ac-cronjob.sh <mode>`.

**⚠️ WICHTIG für Cronjobs:** Der Cronjob-Scheduler interpretiert das GESAMTE `script`-Feld als Dateinamen. `script="ha-ac-cronjob.sh cool"` sucht nach einer Datei namens `ha-ac-cronjob.sh cool` — das schlägt fehl. **Immer einen Wrapper ohne Argumente erstellen:**

```bash
# Wrapper-Script (z.B. ac-ein.sh):
#!/bin/bash
exec /DATA/.hermes/scripts/ha-ac-cronjob.sh cool

# Dann im Cronjob:
# script="ac-ein.sh"  (KEINE Argumente!)
```

### Cronjob anlegen

```bash
# Wrapper-Scripts liegen unter /DATA/.hermes/scripts/:
#   ac-ein.sh  (cool)    ac-aus.sh  (off)
#   ac-heat.sh (heat)    ac-dry.sh  (dry)
#   ac-fan.sh  (fan_only)  ac-auto.sh (auto)

# Dann per Hermes cronjob create:
# schedule = ISO 8601 Timestamp (NICHT "once at ..." Format!)
# script = Wrapper-Name OHNE Argumente (z.B. "ac-aus.sh")
# no_agent = true
```

**Cronjob-Schedule-Format:** Hermes akzeptiert NUR ISO-8601-Timestamps (`2026-06-19T21:37:44+02:00`), nicht das natürliche `"once at 2026-06-19 21:37"`. Letzteres wirft `Invalid schedule`. Immer `date '+%Y-%m-%dT%H:%M:%S%:z'` oder `date '+%Y-%m-%dT%H:%M:%S%:z' -d '+15 minutes'` verwenden.

### WICHTIG: Datum nach Mitternacht

**Nie das Datum aus dem Kopf oder von `date +%s` ableiten.** Nach Mitternacht ist das Kalenderdatum bereits der neue Tag. Immer `date '+%Y-%m-%dT%H:%M:%S%:z'` verwenden, um den exakten ISO-8601-Timestamp zu bekommen.

```bash
# RICHTIG:
date '+%Y-%m-%dT%H:%M:%S%:z'  # → 2026-06-19T00:05:00+02:00

# FALSCH (führt zu still ignorierten Jobs):
# Aus 00:01 CEST + "in 30 min" → 2026-06-18T00:31:00+02:00
# wenn es bereits der 19. Juni ist!
```

### API-Response bei set_hvac_mode

Der Call returned `[]` (leeres JSON-Array) mit HTTP 200 — das ist normal. Die Zustandsänderung (z.B. `off` → `cool`) erscheint erst bei der nächsten Status-Abfrage. Nicht von `[]` irritieren lassen.

## Pitfalls

- **Patches VOR dem ersten HA-Start** — sonst crasht die Integration beim Laden
- **AUX_HEAT-Flag entfernen, aber kein `| \ ` am Zeilenende lassen** — sonst SyntaxError
- **Niemals git clone auf ZimaOS** — git funktioniert dort nicht. Immer Mac + SCP
- **V2-Protokoll = keine Status-Updates** — sieht verbunden aus, tut aber nichts
- **msmart-ng ist der einzige zuverlässige Weg** an echte Tokens zu kommen — MSmartHome Cloud API gibt 3004
- **msmart-ng Cloud-Rate-Limit:** `discover` versucht Cloud-Login und kann mit `Code: 7610 — Too many failed login attempts` fehlschlagen. Wenn Token/Key bereits bekannt sind (aus config_entries oder device-JSON), **Discovery überspringen** — direkt die gespeicherten Werte nutzen. Discovery nur bei Erst-Setup oder Token-Verlust nötig.
- **`climate.turn_on` wird nicht unterstützt!** Nur `set_hvac_mode` verwenden — das schaltet implizit ein. Kein Chainen von Services (kein turn_on + mode + temperature in schneller Folge)
- **`set_hvac_mode` kann beim ersten Mal fehlschlagen** — der Call returned `[]` (HTTP 200), aber das Gerät bleibt `off`. Ursache unbekannt (Timing/Gerät-Timeout). **Workaround:** Nach `set_hvac_mode` 3 Sekunden warten, Status abfragen. Wenn `state` immer noch `off` ist, den exakt gleichen Call wiederholen. Der zweite Versuch greift zuverlässig.
- **Long-Lived Access Token sind gehashed** — nicht direkt als API-Token verwendbar. Stattdessen Refresh-Token-Austausch nutzen (normale Refresh-Tokens brauchen exakt ihre gespeicherte `client_id` im Form-Body)
- **ECO überschreibt Fan-Mode** — Fan NACH ECO setzen wird ignoriert. Entweder ECO (Fan=Auto) ODER manueller Fan (preset_mode=none). Nicht beides.
- **Cronjob `script`-Feld = Dateiname, keine Argumente!** Der Scheduler interpretiert das gesamte Feld als Pfad. `script="ha-ac-cronjob.sh cool"` sucht nach einer Datei die wörtlich so heißt → `Script not found`. **Immer Wrapper ohne Argumente verwenden:** `ac-ein.sh`, `ac-aus.sh`, etc. (liegen unter `/DATA/.hermes/scripts/`).
