Buderus GB192i Smarthome MQTT Integration

Posted on | 879 words | ~5mins

Aktuelle Buderus Gasbrennwertheizungen haben im Standardlieferumfang ein “Smarthome”-Modul des Typs KM-100 (je nach Modell wohl auch KM-50, KM-200 etc) enthalten, dass die Bedienung mit einer proprietären App ermöglicht. Das Modul verknüpft ein IP-Netzwerk mit dem EMS plus Bus-System der Heizung, in dem es die Steuerung und Abfrage einzelner Endpunkte erlaubt. Keinesfalls liefert es die vollständigen Informationen, die über den Bus zur Verfügung stehen würden. Intern bietet dieses Modul jedoch auch die Möglichkeit, es in richtige Smarthome-Systeme wie HomeAssistant zu integrieren. Hier ist die Vorgehensweise kompakt zusammengeschrieben, sodass ihr sie euch nicht aus zig Forenbeiträgen zusammensuchen müsst. Vielen Dank an alle, die das aufwendige Reverse Engineering gemacht haben.

Zuerst sei gesagt, dass es notwendig ist die offizielle App (https://play.google.com/store/apps/details?id=com.bosch.tt.buderus) zu benutzen um ein “Benutzerpasswort” festzulegen. Idealerweise sollte dieses keine Sonderzeichen enthalten und nicht sehr lang sein.

Aus Benutzer- und Gerätepasswort lässt sich ein Cryptkey erzeugen, der für die Verschlüsselung Kodierung der Antwort des Moduls verwendet wird. Am Ende will man dem KM-Modul das Internet wegnehmen, um zu verhindern dass sich dieses aktualisiert und die Schnittstelle reduziert wird. Zudem verwendet das KM-Modul unverschlüsselte XMPP-Server, was in Kombination mit dem Verständnis von Kryptografie beim Hersteller, nicht unbedingt für die Sicherheit des Moduls spricht.

Der ominöse Saltwert für den Cryptkey

Leider geht wohl Bosch Thermotechnik aktuell und in der Vergangenheit gegen die Veröffentlichung des Saltwerts vor mit dem das Passwort “verschlüsselt” wird (“Aus Urheberrechtsgründen”). So ist etwa die Webseite offline, mithilfe derer man sich den Cryptkey schnell erstellen lassen konnte (https://ssl-account.com/km200.andreashahn.info) sowie zahlreiche Repositories (https://web.archive.org/web/20220923100200/https://github.com/mrMoe/km200prometheus/blob/master/crawler.py) und Forenbeiträge die den ominösen Zahlenwert enthalten haben. Zudem gibt es wohl auch unterschiedliche Saltwerte.

Da ich keine Urheberrechtsverletzung begehen möchte, den Wert aber trotzdem dokumentieren möchte und für die Nachwelt erhalten will, hier der mit einem äußerst beliebten Verfahren kodierte Wert (Hint: der dekodierte Wert beginnt mit 867… und endet auf 1e4):

ODY3ODQ1ZTk3YzRlMjlkY2U1MjJiOWE3ZDNhM2UwN2IxNTJiZmZhZGRkYmVkN2Y1ZmZkODQyZTk4OTVhZDFlNAo=

Wie der Cryptkey daraus erzeugt wird, ist aus Code des oben verlinkten Repos ersichtlich:

    def create_decryption_key(self, gateway_password, private_password):
        part1 = MD5.new()
        part1.update(gateway_password.replace('-', '').encode() + bytes.fromhex(BUDERUS_MAGIC_BYTES))

        part2 = MD5.new()
        part2.update(bytes.fromhex(BUDERUS_MAGIC_BYTES) + private_password.encode())

        return part1.digest()[:32] + part2.digest()[:32]

Wer den Saltwert immer noch nicht finden kann, kann auch hier nachschauen: https://forum.fhem.de/index.php?topic=25540.0 dort steht er direkt im ersten Beitrag.

Die HTTP-Schnittstelle des KM-xx Moduls

Das KM-xx Modul bietet eine HTTP-Schnittstelle an um Daten abzufragen und Einstellungen zu setzen.

Daten abrufen

Im folgenden ein Beispielskript, das Daten abruft, dekodiert und per MQTT weiterleitet. Dieses ist natürlich sehr einfach aufgebaut aber bei mir sehr erfolgreich im Einsatz. Hier sei nochmal den Forenmitgliedern gedankt, die die zeitaufwendige Arbeit des Reverse Engineerings getätigt haben und auf dessen Arbeit wir hier einfach aufbauen können! Insbesondere aus diesem Beitrag im FHEM Forum: https://forum.fhem.de/index.php?topic=25540.0 und Symcon Wiki (Link ist tot): http://www.ip-symcon.de/forum/threads/25103-Buderus-Logamatic-Web-KM200

import paho.mqtt.client as mqtt

import requests
import base64

import json

import binascii
from Crypto.Cipher import AES
#from pyaes import PADDING_NONE, AESModeOfOperationECB, Decrypter

# see also https://github.com/rthill/buderus/blob/master/__init__.py
# is GPL 3 or later

# userpassword: REDACTED
# deviepassword: REDACTED

# generated, TODO generate from user & device password
CRYPTKEY = "REDACTED"

IP_ADDRESS = "192.168.1.11"


key = binascii.unhexlify(CRYPTKEY)
INTERRUPT = '\u0001'
PAD = '\u0000'

def decrypt_km200(enc):
    decobj = AES.new(key, AES.MODE_ECB)
    data = decobj.decrypt(base64.b64decode(enc))
    data = data.rstrip(PAD.encode()).rstrip(INTERRUPT.encode())
    return data.decode('utf-8')

def query_data(path):
    header = {"Accept": "application/json", "User-Agent": "TeleHeater/2.2.3" }

    #try:
    url = 'http://' + IP_ADDRESS + path
    # self.logger.debug("Buderus fetching data from {}".format(path))
    resp = requests.get(url, headers=header)
    return decrypt_km200(resp.text)

def query_data_and_push_to_mqtt(url):
    data_value = json.loads(query_data(url))#['value']

    # replace ACTIVE/on with 1 and 0 for easier parsing later


    if data_value['value'] in ["ok","ACTIVE","yes","on"]:
        data_value['value']=1
    elif data_value['value'] in ["error","no","INACTIVE","off"]:
        data_value['value']=0
    elif data_value['value'] in ["maintenance"]:
        data_value['value']=-1

    data_value.pop('type',None)
    data_value.pop('state',None)
    data_value.pop('writeable',None)
    data_value.pop('maxValue',None)
    data_value.pop('minValue',None)

    data_value.pop('recordable',None)
    data_value.pop('unitOfMeasure',None)
    data_value.pop('allowedValues',None)

    client.publish("heizung"+url, json.dumps(data_value))


cert_location = "/etc/ssl/certs/ca-certificates.crt"
client = mqtt.Client()
client.tls_set(cert_location)
client.username_pw_set(username="for_heizung", password="""REDACTED""")
client.connect("MQTT_HOST", 8883, 60)


urls_to_fetch = [
# general
#isHeizungOk
"/system/healthStatus",
#outDoorTemp
"/system/sensors/temperatures/outdoor_t1",
#switchTemp
"/system/sensors/temperatures/switch",

# Heizung
#Vorlauf_temp
"/system/sensors/temperatures/supply_t1",
#r  cklaufTemp
"/system/sensors/temperatures/return",
#supplytemp
"/heatingCircuits/hc1/actualSupplyTemperature",
#pumpModul
"/heatingCircuits/hc1/pumpModulation",
#isOn
"/heatingCircuits/hc1/status",
#powerUsedForHeating
"/heatSources/actualCHPower",

# Hotwater
#hotwaterFlow
"/dhwCircuits/dhw1/waterFlow",
#hotwaterTemp
"/dhwCircuits/dhw1/actualTemp",
#powerUsedForHotwater
"/heatSources/actualDHWPower",
#hotwaterTemp_2
"/system/sensors/temperatures/hotWater_t2",

# solar
#solarTankTemp
"/solarCircuits/sc1/dhwTankTemperature",
#solarModuleTemp
"/solarCircuits/sc1/collectorTemperature",
#solarPump
"/solarCircuits/sc1/pumpModulation",
#solarYield
"/solarCircuits/sc1/solarYield",
#isSolarUsed
"/solarCircuits/sc1/actuatorStatus",
"/solarCircuits/sc1/status",



# gas
#gasModulation
"/heatSources/hs1/actualModulation",
#gasPower
"/heatSources/hs1/actualPower",
#gasPower
"/heatSources/hs1/flameStatus",



#supplyTemperature
"/system/appliance/actualSupplyTemperature",
#othersupplyTemperature
"/heatSources/actualSupplyTemperature",

"/heatSources/energyMonitoring/consumption",
"/heatSources/energyMonitoring/startDateTime",

#x
"/heatingCircuits/hc1/suWiThreshold",

"/heatSources/workingTime/totalSystem",
"/heatSources/workingTime/centralHeating",
"/heatSources/numberOfStarts",
"/heatSources/systemPressure",


]


for url in urls_to_fetch:
    query_data_and_push_to_mqtt(url)


# https://github.com/demel42/IPSymconBuderusKM200/blob/master/docs/datapoints.md

Einstellungen ändern

Im folgenden ein kleines Skript, das auf einem MQTT-Topic horcht und entsprechend den Sommer- bzw. Wintermodus der Heizung umstellt. Auch dieses Skript ist nur ein Proof-of-Concept, das aber hervorragend funktioniert.

import paho.mqtt.client as mqtt
import requests
import base64
import json
import binascii
from Crypto.Cipher import AES

# generated, TODO generate from user & device password
CRYPTKEY = "TODO"

IP_ADDRESS = "192.168.1.1"


key = binascii.unhexlify(CRYPTKEY)
INTERRUPT = '\u0001'
PAD = '\u0000'
    
def decrypt_km200(enc):
    decobj = AES.new(key, AES.MODE_ECB)
    data = decobj.decrypt(base64.b64decode(enc))
    data = data.rstrip(PAD.encode()).rstrip(INTERRUPT.encode())
    return data.decode('utf-8')

def encrypt_km200(plain):
    plain = plain.encode() + (AES.block_size - len(plain) % AES.block_size) * PAD.encode()
    encobj = AES.new(key, AES.MODE_ECB)
    data = encobj.encrypt(plain)
    return base64.b64encode(data)
    
def set_value(path, value):
    data = {"value": value}
    data = encrypt_km200(json.dumps(data))
    header = {"Content-Type": "application/json", "User-Agent": "TeleHeater/2.2.3" }
    try:
        url = 'http://' + IP_ADDRESS + path
        r = requests.put(url, data=data, headers=header)
        print("Buderus returned {}: {}".format(r.status_code, r.content))
    except Exception as e:
        print("Buderus error happened at {}: {}".format(url, e))
        return None

def query_data(path):
    header = {"Accept": "application/json", "User-Agent": "TeleHeater/2.2.3" }
    
    url = 'http://' + IP_ADDRESS + path
    resp = requests.get(url, headers=header)
    return decrypt_km200(resp.text)
    

def on_message(client, userdata, message):
    if(message.topic=="smarthome/heizung/set"):
        message_str=str(message.payload.decode("utf-8"))
        if(message_str=="ON"):
            set_value("/heatingCircuits/hc1/suWiSwitchMode","automatic")
        elif(message_str=="OFF"):
            set_value("/heatingCircuits/hc1/suWiSwitchMode","off")

client = mqtt.Client(client_id="clientid")
client.connect("localhost", 1883, 60)
client.on_message=on_message
client.subscribe("smarthome/heizung/set")
client.loop_start()

while(True):
    None