Full local control of a Kospel EKCO.MN3 electric boiler (+ C.MG3 mixing module) over RS485 — with an optional local-LLM “AI caretaker” that steers heating around dynamic electricity prices.
Born when the original Kospel C.MI internet module started dying. Instead of replacing it, this project puts an ESP32 (Kincony KC868-A6) on the RS485 bus as the Modbus master and re-implements the C.MI’s entire feature set in ESPHome — then goes further: price-aware schedules, per-room window failsafe, cost accounting good enough to bill tenants, and a supervised autonomous mode with hard guardrails.
Disclaimer: this controls real heating hardware over an undocumented (reverse-engineered) protocol. Registers were mapped from the C.MI’s local web app and live bus sniffing on EKCO.MN3 + C.MG3. Use at your own risk. Never run the ESP master and the C.MI on the bus at the same time (the firmware includes relay-based bus handover if you wire the C.MI through relays 5/6).
What you get
- 1:1 C.MI feature port — temperatures, power, pressure/flow, operating modes, weekly programs + daily-program editor (all 4 timetables: CO / CWU / circulation / C.MG3), heating curves, disinfection (anti-legionella), pump config, special modes (party/vacation/turbo), RTC sync from NTP, error decoding — ~150 entities in Home Assistant.
- Write pending/confirm framework — a changed setting holds in the UI until the heater confirms it (no snap-back); failures surface as a red banner + notification.
- AI caretaker (optional) — an AppDaemon app driving a local LLM (Ollama): analyses state, weather forecast, 6 h trends and hourly prices (pstryk.pl); writes proposed daily programs into the heater’s unused program 8 (shadow mode) or actively points the week at them (Autonomiczny) with watchdog + automatic rollback. Learns the household’s hot-water rhythm from tank-temperature draw detection — no water meter needed.
- Price-driven power plan (opt-in) — the heater has no native power schedule, so the AI pushes a rolling 24 h plan into the ESP (expensive hours 12 kW, normal 20, cheap 24) and the ESP executes it with local guards even with HA down: comfort floor, tank below 35 °C (heavy hot-water usage mid-peak → full power to recover, then re-cap) and anti-legionella always win; a stale plan (>26 h) safely reverts to full power; your own setting returns when autonomy exits.
- Billing-grade energy/cost stack — CO/CWU split power → kWh meters → PLN totals at exact hourly prices (Energy-Dashboard-ready, statistics kept forever).
- Wall-panel dashboard (1080p landscape tablet) + appliance advice card (“Prąd drogi — nastaw zmywarkę na 13:00, o 60% taniej”).
- Optional: Fibaro TRV integration — per-room temperatures and open-window detection via a Z-Wave JS Pi; the ESP receives a UDP feed directly from the Pi, so the windows-open → pause heating failsafe works even with Home Assistant down.
Screenshots
The AI caretaker’s analysis and controls on the main dashboard:
Wall-panel view (1080p landscape tablet, no scrolling) with the appliance price advisor:
Settings view — Ollama engine, Pstryk API key, autonomy guardrails, Z-Wave controller health:
Hardware
| Part | Notes |
|---|---|
| Kospel EKCO.MN3 | slave 0x65; C.MG3 mixing module slave 0x69 |
| Kincony KC868-A6 (ESP32) | built-in RS485; 9600 8N1, Modbus RTU, func 0x03/0x10, little-endian on wire |
| RS485 A/B | to the heater’s C.MI connector; optionally route the C.MI through relays 5/6 for bus handover |
| (optional) Raspberry Pi + Z-Wave stick | zwavejs2mqtt/Z-Wave JS UI for Fibaro FGT-001 TRVs |
| (optional) any box with a GPU | Ollama for the AI caretaker (a ~30B-class model works well) |
Install
→ New here? Follow the detailed step-by-step guide: docs/INSTALL.md (wiring, first USB flash, HA packages, dashboards import, Ollama + AppDaemon setup, optional Z-Wave, verification checklist, troubleshooting).
The short version:
1. Firmware (required)
cd esphome
cp secrets.yaml.example secrets.yaml # fill in; never commit
# Review the EDIT ME markers in gen_master.py (static IP, UDP feed address)
python3 gen_master.py # generates kc868-a6-heater-master.yaml
esphome run kc868-a6-heater-master.yaml
Add the device to Home Assistant (ESPHome integration, encrypted API). Physically disconnect the C.MI (or wire it through relays 5/6 and use the “Bus owner” switch).
2. Home Assistant packages (required for costs/AI)
Enable packages in configuration.yaml, then copy from homeassistant/packages/:
kospel_helpers.yaml— input helpers the AI expects (modes, thresholds, Ollama address)kospel_energia.yaml— CO/CWU power split, kWh meters, PLN cost integrals + daily/monthly meters. Point the Energy Dashboard atsensor.kospel_energia_co/cwuwith your hourly price entity. Totals live in long-term statistics → season billing = end minus start.
homeassistant/dashboards/wall_panel.json is a ready wall-panel view (paste into a dashboard via raw editor; fits 1080p landscape tablets without scrolling). It references two aggregate sensors from the optional TRV setup (sensor.dom_otwarte_okna, sensor.dom_temperatura_min) — remove those two tiles or point them at your own sensors if you skip Z-Wave.
3. Enable the AI caretaker (optional)
-
Run Ollama somewhere on your LAN and pull a model (e.g.
ollama pull gemma4:26b-a4b-it-qat). -
Install the AppDaemon add-on. Copy
appdaemon/kospel_llm.pyandapps.yaml.example(asapps.yaml) into/addon_configs/a0d7b954_appdaemon/apps/. -
Configure the app the AppDaemon-canonical way —
apps.yamlargs with!secret(seeapps.yaml.example+secrets.yaml.example):ollama_hostandpstryk_api_key. The Ustawienia dashboard view offers runtime overrides on top (a password-typeinput_text.kospel_pstryk_api_keyhelper and the Ollama host field) — handy, but note an input_text state is readable by any logged-in HA user; secrets.yaml is the more private home for the key. Resolution order: UI helper > apps.yaml > legacy.pstryk-keyfile. -
Set
input_text.kospel_llm_hostto your Ollama URL, pick the model, and switchinput_select.kospel_llm_trybthrough the maturity ladder:- Doradca — analysis text only, writes nothing;
- Propozycje (shadow) — additionally writes proposed daily programs into program 8 (inactive unless you point a weekday at it) — run this for a few days and read its plans;
- Autonomiczny — backs up your weekly assignments, points CO/CWU/circulation at program 8, and keeps refreshing it. Exit the mode (or any watchdog trip) restores your backup automatically.
Guardrails in autonomous mode: boiler alarm (debounced), room-temperature floor (
input_number.kospel_ai_min_pokoj), ESP-offline grace, daily write budget, and the AI can only ever downgrade itself — it never enables autonomy.
4. Fibaro TRVs via Z-Wave (optional)
Skip this entirely if you don’t have Z-Wave radiator heads — everything above works without it.
- On the Pi running Z-Wave JS: install
zwave-agent/zwave_agent.py(seezwave-agent.service.example; edit theTRVnode-id→room map). The agent serves/trv.json+/health, self-heals the zwave-js container (restart/rollback-safe update), and pushes{okna, min_temp}to the ESP by UDP every 30 s — deliberately push-over-UDP: an HTTP pull from the ESP proved to disrupt its HA API connection (see Lessons). - Copy
homeassistant/packages/kospel_zwave.yaml(edit the Pi IPs) for health sensors + restart/update REST commands, and optionally runzwave-agent/selfheal_automations.pyto create the down→restart / stale→update / weekly-update automations.
Modbus map (highlights)
All values little-endian on the wire (byte-swapped vs Modbus convention). Slaves: heater 0x65, C.MG3 0x69 (rejects coalesced wide reads — use single-register commands).
| Area | Registers |
|---|---|
| Temperatures (CO in/out, CWU, room, outside…) | 0x0B3B…0x0B50 region |
| Status / mode / error words | 0x0B51, 0x0B55 (bit3 = Lato, bit5 = Zima), 0x0B52 |
| Setpoints (DHW eco/comfort, room, CO max/manual, curves) | 0x0B62…0x0B8D |
| Daily programs (5 slots × start/stop/level) | CO base 0x0C1C, CWU 0x0C9E, circulation 0x0D20, C.MG3 0x0B90; program N = base + 15·(N−1) |
| Weekly assignments (Mon..Sun) | CO 0x0C94-0x0C9A, CWU 0x0D16-0x0D1C, circ 0x0D98-0x0D9E, C.MG3 0x0C08-0x0C0E |
| RTC | 0x0AF6 (7 regs), written hourly from SNTP |
| Keep-alive | write {0x0000, 0x0100} to 0x0BAE every 10 s (C.MI heartbeat) |
Some config registers only latch when the config word (0x0B55/0x0B54) is re-written in the same burst — the generator handles these “gated” writes.
Config flags & season
The heater’s season and subsystem-enable flags live in one word (0x0B55). There’s a non-obvious ordering rule (enable the DHW tank before selecting summer) and a safe recovery procedure — see docs/CONFIG-FLAGS.md.
Lessons learned (the expensive ones)
- ESPHome
http_requestpolling can reset your API connection. Periodic 0.5 sunavailableblips in HA quantized exactly to the poll period (proved by switching the interval to a prime 127 s). Fix: invert to UDP push. If you see dotted graphs, suspect any periodic network activity in your firmware first. post_connect_roamingandpower_save_mode: light(ESP32 default) both cause drops on weak RSSI for stationary devices — disable both.- ESPHome’s API allows 5 concurrent connections — stale
esphome logssessions can starve Home Assistant itself. - A boiler alarm sensor computed from a not-yet-read register briefly reads garbage after every reboot — guard NaN, and debounce watchdogs that act on it.
- Season needs the DHW tank enabled first — writing summer while the tank is off silently reverts (see docs/CONFIG-FLAGS.md). The heater owns
0x0B55; enable preconditions, then season.
Roadmap
- Custom Home Assistant integration (
custom_components/) with a config-flow settings page — so the project shows up as a real HA integration (Ollama host, Pstryk key, etc.) instead of AppDaemon + helpers. - Symbolic config-flag write layer — the C.MI writes flags like
FLAG_DHW_ACTIVATION_NO_YESas named transactions (server-side register mapping). The ESP already covers season + the main enable flags via per-bit RMW switches (see docs/CONFIG-FLAGS.md); porting the remaining gated config groups (buffer/OPERATING_MODE, DHW int/ext with supply temp) is the last parity gap.


