Kermi Heat Pump X-Center Integration (kind of) - Without Modbus

Hi there!

I was a silent reader till now, but now i finally registered and thought i maybe can give something back to the community. I have a Kermi Heat Pump, but no Modbus. I’ve read a thread here where some other people searched for it too, but no solution.

So here’s what i did in summary:

  • In installed the MQTT Integration in HA and configured it
  • I have a Python Script on my HA-Server that logs in to Heat Pump Web Interface and grabs the Data from the API and writes it to MQTT (Autodiscovery and States)
  • This script is run through automation and the shell_command integration every minute

The downsides of this integration are:

  • You have to have specific items in your favorites in the heat pump web interface
  • If the Web-Interface in any way changes (URLs, API-JSON-Strucutre, …) the script may break
  • It’s only one-way, you only can read data, not write
  • The Heat Pumps may be different in version and functions and some adjustments in the code may be neccessary, but the script should give you a good start to understand everything

If this is fine for you and you know about the things i talked about, here’s my scraper python-script:

import json
import requests
import paho.mqtt.client as mqtt
import time
import unicodedata
import re

# Konfigurationswerte
DEBUG = False  # Auf True setzen, um Log-Ausgaben zu erhalten; sonst auf False
LOGIN_URL = "http://<ip-of-your-hp>/api/Security/Login"
DATA_URL = "http://<ip-of-your-hp>/api/Favorite/GetFavorites/00000000-0000-0000-0000-000000000000"
MQTT_BROKER = "core-mosquitto"
MQTT_PORT = 1883
MQTT_USERNAME = "<mqtt-username>"     # Kann auch ein HA-Benutzer sein
MQTT_PASSWORD = "<mqtt-password>"     # Kann auch ein HA-Benutzer sein
MQTT_TOPIC_PREFIX = "mqtt"      # Sensorwerte
DISCOVERY_PREFIX = "mqtt-ad"    # Auto-Discovery Prefix für HA

PASSWORD = "<webinterface-login-password>"  # i.d.R. letzte 4 Stellen der Seriennummer
WP_DEVICE_NAME = "wp_kermi"

def normalize_key(name):
    """Erzeugt einen konsistenten Schlüssel: kleingeschrieben, ohne Umlaute und Sonderzeichen."""
    name = name.lower()
    name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii')
    name = re.sub(r'[\s-]+', '_', name)
    name = re.sub(r'[^a-z0-9_]', '', name)
    return name

def log_extracted_data(extracted):
    """Schreibt die vom Scraper verarbeiteten Sensorwerte (Name, Wert, Einheit) in die Log-Datei."""
    if DEBUG:
        try:
            with open("kermi_scraper.log", "a", encoding="utf-8") as f:
                f.write(json.dumps(extracted, ensure_ascii=False, indent=2))
                f.write("\n\n")
        except Exception as e:
            print(f"Fehler beim Schreiben ins Log: {e}")

def login():
    session = requests.Session()
    payload = {"Password": PASSWORD}
    headers = {"Content-Type": "application/json"}
    response = session.post(LOGIN_URL, json=payload, headers=headers)
    print(f"Login-Response Code: {response.status_code}")
    if response.status_code != 200:
        print(f"Fehler beim Login: {response.text}")
        response.raise_for_status()
    try:
        data = response.json()
        if not data.get("isValid", False):
            print("Fehler: Login war nicht erfolgreich!")
            return None
    except json.JSONDecodeError:
        print("Fehler: Keine gültige JSON-Antwort beim Login.")
        return None
    return session

def fetch_data(session):
    payload = {"WithDetails": True, "OnlyHomeScreen": True}
    headers = {"Content-Type": "application/json"}
    response = session.post(DATA_URL, json=payload, headers=headers)
    print(f"Datenabruf-Response Code: {response.status_code}")
    if response.status_code != 200:
        print(f"Fehler beim Abrufen der Daten: {response.text}")
        response.raise_for_status()
    try:
        return response.json()
    except json.JSONDecodeError:
        print("Fehler: Die API hat keine gültige JSON-Antwort zurückgegeben.")
        print(f"Antworttext: {response.text}")
        return None

def extract_data(api_response):
    """Extrahiert Daten aus `ResponseData` und `VisualizationDatapoints`."""
    all_sensors = []

    # Daten aus `ResponseData`
    for item in api_response.get("ResponseData", []):
        if "$type" not in item or "DatapointValue" not in item or "DatapointConfig" not in item:
            continue
        config = item["DatapointConfig"]
        value = round(item["DatapointValue"]["Value"], 2) if isinstance(item["DatapointValue"]["Value"], float) else item["DatapointValue"]["Value"]
        name = config["DisplayName"]
        unit = config.get("Unit", "")
        all_sensors.append({
            "name": name,
            "value": value,
            "unit": unit
        })

    # Zusätzliche Daten aus `VisualizationDatapoints`
    for item in api_response.get("ResponseData", []):
        if "VisualizationDatapoints" in item:
            for datapoint in item["VisualizationDatapoints"].get("$values", []):
                config = datapoint["Config"]
                value = round(datapoint["DatapointValue"]["Value"], 2) if isinstance(datapoint["DatapointValue"]["Value"], float) else datapoint["DatapointValue"]["Value"]
                name = config["DisplayName"]
                unit = config.get("Unit", "")
                all_sensors.append({
                    "name": name,
                    "value": value,
                    "unit": unit
                })
    
    return all_sensors

def send_mqtt(data):
    if not data:
        print("Keine Daten vorhanden, MQTT wird nicht gesendet.")
        return

    #client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) # Funktioniert nicht im HA docker container
    client = mqtt.Client()
    client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
    client.connect(MQTT_BROKER, MQTT_PORT, 60)

    published = set()
    extracted_sensors = []  # Hier werden die extrahierten Sensorwerte gesammelt

    for item in data:
        name = item["name"]
        sensor_key = normalize_key(name)
        if sensor_key in published:
            continue
        published.add(sensor_key)
        value = item["value"]
        unit = item.get("unit", "")

        # Discovery-Nachricht erstellen
        discovery_topic = f"{DISCOVERY_PREFIX}/sensor/{sensor_key}/config"
        state_topic = f"{MQTT_TOPIC_PREFIX}/{sensor_key}"
        config_payload = {
            "name": f"Kermi: {name}",
            "state_topic": state_topic,
            "value_template": "{{ value_json.value }}",
            "unit_of_measurement": unit,
            "device_class": None,
            "icon": "mdi:flash",
            "unique_id": f"kermi_{sensor_key}",
            "availability_topic": "mqtt/status",
            "device": {
                "name": WP_DEVICE_NAME,
                "identifiers": [WP_DEVICE_NAME]
            }
        }

        client.publish(discovery_topic, json.dumps(config_payload), retain=True)
        print(f"Discovery gesendet: {discovery_topic}")

    # Kurze Pause, damit HA die Discovery-Nachrichten verarbeiten kann
    time.sleep(3)

    # State-Nachrichten senden und extrahierte Sensorwerte sammeln
    for item in data:
        name = item["name"]
        sensor_key = normalize_key(name)
        state_topic = f"{MQTT_TOPIC_PREFIX}/{sensor_key}"
        value = item["value"]
        unit = item.get("unit", "")

        client.publish(state_topic, json.dumps({"value": value, "unit": unit}), retain=False)
        print(f"Sende MQTT: {state_topic} -> {value} {unit}")
        extracted_sensors.append({"name": name, "value": value, "unit": unit})

    log_extracted_data(extracted_sensors)
    client.disconnect()

try:
    session = login()
    if session:
        print("Login erfolgreich!")
        api_response = fetch_data(session)
        if api_response:
            print("Daten erfolgreich abgerufen!")
            extracted_data = extract_data(api_response)
            send_mqtt(extracted_data)
            print("Daten erfolgreich gesendet!")
        else:
            print("Fehler: Die API hat keine verwertbaren Daten geliefert.")
    else:
        print("Fehler: Kein gültiges Session-Objekt erhalten.")
except Exception as e:
    print(f"Fehler: {e}")

Here’s an example how this could look in HA:

If you need help setting up, just ask :slight_smile:

Otherwise, have fun!

Greetings

Hi,
I tried to integrate this in my HA installation but I’m a newbie to HA and MQTT as well and my python skills are towards zero.
I wrote the python script via copy and paste and started it. It shows “Login erfolgreich” and “Daten erfolgreich abgerufen”, so I guess the connection to the heat pump is working and only extracting or sending is not working because then it shows Fehler: ‘NoneType’ object has no attribute ‘get’
Perhaps I can get some help to get it working. The heat pump is controlled via Kermi hydrobox pro.

Hi @morgindale, many thanks for sharing!
Did you have to enable local API connections to the x-center somewhere?
For my x-center x40 it seems that only the ports 21 (FTP) and 23 (Telnet) are open; no HTTP/HTTPS (80/443).

I’m currently accessing it through the RemoteControl web interface (x-center …). However, it seems that this API cannot be used by custom client applications.
Thanks in advance for any help!

Hey everyone! So sorry that i didnt respond earlier, this forum didnt give me any notification :-/

@brox1234 Hm never seen the error, maybe something specific to your installation. But i figured out that ChatGPT is really helpful with mqtt and python stuff, just give it my scraper and tell it the error you get, im sure it will tell you what you have to change. If you need any further help, you can also dm me (i hope i get the notification then).

@lfleck hm no, not really - i’m using it like you, the same address i use for the web interface. What the python script does is, it authenticates to the same api the webinterface uses, also over http and reads the json the api gives back an normally shows on the webinterface then. Do you get any errors or something if you run the scraper?