Python client for the Kospel C.MI electric heater HTTP API.
This library provides a Python client for controlling Kospel C.MI electric heaters via their HTTP API. It is designed for integration with Home Assistant and other automation systems, and supports device discovery, register-based control, and offline development with a YAML simulator.
- Async-first: Built on
asyncioandaiohttpfor non-blocking I/O - Type-safe: Strict type hinting throughout
- Device-specific API: Explicit properties and async setters on
Ekco_M3 - Simulator-capable: Full simulator for offline development and testing (no hardware required)
- Protocol-based: Decoder/encoder interfaces via Python
Protocoltypes - Device discovery:
probe_device()anddiscover_devices()to find Kospel devices on the network (no device_id required)
- Heater mode control (OFF, SUMMER, WINTER, PARTY, VACATION, MANUAL)
- CWU (water) mode and temperatures (economy, comfort)
- Manual heating temperature
- Device discovery (CLI
kospel-discover+ Python API) - Register scanner and live scanner tools (
kospel-scan-registers,kospel-scan-live) - YAML backend for offline testing (no hardware required)
# With uv (recommended)
uv add kospel-cmi-lib
# With pip
pip install kospel-cmi-lib- Install:
uv add kospel-cmi-liborpip install kospel-cmi-lib - Discover device: Run
kospel-discoveror useprobe_device(session, "192.168.x.x")to getapi_base_url - Connect and read: Create
Ekco_M3withHttpRegisterBackend(session, api_base_url)and callrefresh()
Create a register backend (HTTP or YAML) and pass it to Ekco_M3.
When using HttpRegisterBackend, call aclose() or use the controller as an
async context manager to release the HTTP session when done.
Recommended: async context manager (resources released automatically):
import asyncio
import aiohttp
from kospel_cmi.controller.device import Ekco_M3
from kospel_cmi.kospel.backend import HttpRegisterBackend, YamlRegisterBackend
from kospel_cmi.registers.enums import HeaterMode
async def main() -> None:
api_base_url = "http://192.168.1.1/api/dev/65" # Replace with your heater URL
async with aiohttp.ClientSession() as session:
backend = HttpRegisterBackend(session, api_base_url)
async with Ekco_M3(backend=backend) as controller:
await controller.refresh()
print(controller.heater_mode) # Read property
await controller.set_heater_mode(HeaterMode.MANUAL) # Write immediately
# Session and controller resources released here
asyncio.run(main())Alternative: explicit aclose() (for long-lived integrations):
controller = Ekco_M3(backend=HttpRegisterBackend(session, api_base_url))
try:
await controller.refresh()
# ... use controller ...
finally:
await controller.aclose()For offline development or tests, use the YAML backend (no HTTP, no close needed):
backend = YamlRegisterBackend(state_file="/path/to/state.yaml")
controller = Ekco_M3(backend=backend)
await controller.refresh()CLI — scan network and list devices (no config needed):
kospel-discover # Scans common subnets
kospel-discover 192.168.101.0/24 # Scan specific subnetPython API:
import aiohttp
from kospel_cmi import probe_device, discover_devices
async with aiohttp.ClientSession() as session:
info = await probe_device(session, "192.168.101.49")
if info:
print(f"Found: {info.serial_number}, {info.api_base_url}")
found = await discover_devices(session, "192.168.101.0/24")
for device in found:
print(device.host, device.serial_number, device.api_base_url)import asyncio
import aiohttp
from kospel_cmi.controller.device import Ekco_M3
from kospel_cmi.kospel.backend import HttpRegisterBackend
from kospel_cmi.registers.enums import CwuMode, HeaterMode
async def main():
async with aiohttp.ClientSession() as session:
backend = HttpRegisterBackend(session, "http://192.168.1.1/api/dev/65")
async with Ekco_M3(backend=backend) as controller:
await controller.refresh()
# Manual heating: mode + temperature in one call (recommended)
await controller.set_manual_heating(22.0)
# Or use individual setters (each writes immediately)
await controller.set_heater_mode(HeaterMode.WINTER)
await controller.set_manual_temperature(22.0)
# Water: set mode and temperature separately
await controller.set_water_mode(CwuMode.COMFORT)
await controller.set_water_comfort_temperature(38.0)
await controller.set_water_economy_temperature(35.0)
asyncio.run(main())Module-specific documentation is co-located with the code (GitHub automatically displays these when browsing directories):
- kospel/ - HTTP API endpoints and protocol
- registers/ - Register encoding, decoding, and mappings
- controller/ - Ekco_M3 device class
- tools/ - Register scanner and live scanner for reverse-engineering
- Documentation Index - Overview of all docs and suggested reading order
- Development Guide - Contributing and extending the library
- Architecture - System design, layers, components, and data flow
- Technical Specs - Implementation details, data formats, protocols, testing, and coding standards
- No authentication (assumes local network access)
- HTTP only (no HTTPS)
- Single device config (
kospel_cmi_standard) — more variants planned for v2
- Local control - basic device functions can be operated using the library
- Robust interface - an interface for 3rd party tools (i.e., Home Assistant integration)
- Reverse-engineering toolset -
kospel-scan-registersandkospel-scan-livefor exploring registers
- Support multiple device types
- Device discovery
- Advanced state management (error and warning flags, fault detection, debug)
This library was reverse-engineered from JavaScript code used in the heater's web interface. Key findings:
- Register encoding uses little-endian byte order
- Flag bits are used for boolean settings within registers
- Temperature and pressure values are scaled for precision
- Read-Modify-Write pattern is required for setting flag bits
Apache License 2.0