How to integrate Buderus KM271 serial data device into HA

Hello,
I am new to home automation and have no experience.

I want to monitor my Buderus heating system and log/display the data into HA.

The Buderus heating system is controlled by a Logamatic 2107M controller in which I plugged a KM271 communication controller. This controller is actually connected via a serial to usb cable to a computer running Windows and openhab.

![4f8bcae2-8e33-48e3-83c5-586a08547092|690x392]

![3c84d405-d622-4889-8cbe-aad9b948d06f|454x500]

There is a python script, scripted not by me, which will read the data from the serial port and sent these per Rest Api to an openhab server.

It working well,

but I would like to replace Openhab with HA.

Who can help me?

tried the search button??

and if you are only interested in the basic functions:

Yes.

This is an another piece of hardware that I didn’t have.

Hi Janihani,

but there are options:

  • Install ESPhome on HA, take some ESP32-Board and use my esphome_component to connect it easily
  • Use my OpenSource KM271-WiFi-Board and replace the old (non-WiFi) Buderus-KM271 with it

I sell the latter one on Tindie for half the price of the original KM271, but there are only 2 pieces left from my last batch and I’m not sure, if I’ll restock again.

Regards,
Daniel

They are very cheap, for less than €10,- you are finished :wink:

It is right, that the ESP32 boards are cheap, but you still need some other parts (the original KM271 itself, some case, some power supply, an RS232 converter and cables, a power socket near the ESP32,…).

With my KM271 WiFi version, you just need to plug it in inside the control unit and you are done. And since it is so easy, I already shipped more than 200 of it, but you are right, I’m now finished.

I think this was the last batch I ordered. 99% of the people that still have such an old Buderus oil burner and want to integrate it into their smart home have my board already.

It is quite some work for me to order it, assemble the options, package and ship it. I’l concentrate on new projects now. The board is open source and everybody could order the board from PCBways (also an assembled version) directly.

Apparently he has that already, as his heating is currently interfaced to openhab already :thinking:

Then it is easy to go with ESPhome and the km271 component :slight_smile:

First of all I would like to say thanks for the answers.

Maybe you’re right, but I’ve never worked with an ESPHome before, so it’s new to me too.

Should it really be that easy?
I think I need to program the esphome and probably need to do more to get the data transferred to HA.

I was actually hoping to get tips on how to integrate the existing km271 hardware and software into HA.

Since the existing Python script reads the data from the hardware and send the data to openhab via Rest API, I thought about changing the script so that the data are sent to HA.

In the meantime, I watched a few videos and read a lot of posts.

The following video explains very well to me how to create own sensors and send data to them.

I created a sensor called Außentemperatur in configuration.yaml

mqtt:
  sensor:
    - name: "Außentemperatur (Heizung)"
      state_topic: "stat/logamatic/Aussentemperatur"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°"

With MQTT Explorer I send a Value to stat/logamatic/aussentemperatur

display temperature

Meanwhile I found a python script example , which send data to my sensor.

import paho.mqtt.client as mqtt
BROKER_ADDRESS = "192.168.1.1"
PORT = 1883
QOS = 1
Username = "mqttuser"
Password = "mypassword"
TOPIC = "stat/logamatic/Aussentemperatur"
DATA = "7"
client = mqtt.Client()
client.username_pw_set(Username, Password)
client.connect(BROKER_ADDRESS, PORT)
print("Connected to MQTT Broker: " + BROKER_ADDRESS)
client.publish(TOPIC, DATA, qos=QOS, retain=True)
client.loop()
print("Send to Topic: " + TOPIC + ", Message: " + DATA )
client.disconnect()

temperaturverlauf

After learning a lot about MQTT in the last few days, it’s time to get started with Python.

If there is anyone who can help me convert the Python script, please feel free to contact me.

Mqtt is another option,
Maybe Appdaemon or PyScript works for you?

But yeah, ESPHome is maybe not as difficult as you think it is, and very diverse too :stuck_out_tongue:
I’ve converted many ‘smart’ devices to ESPHome, and also build quite a few of my own :thinking:

You should really have a quick look at ESPhome. For many components, you can just add some lines of YAML config to get them into Home Assistant…

1 Like

In the past days I worked on the python script, that is now running fine with a KM271 Communikation module and a serial 2 usb cable.

#!/usr/bin/python3
# -*-coding:Utf-8 -*
# Credits to:  http://foto-paintings.de/index.php/hausautomatisierung/12-heizung/16-test
#         and  https://homematic-forum.de/forum/viewtopic.php?f=18&t=26955&start=40

# Changed by Janihani 
# Version date 30.12.2023

from datetime import datetime
from urllib.request import urlopen
from http.server import BaseHTTPRequestHandler, HTTPServer
import time as t
import binascii
import c3964r
import threading
import http.client
import socket
import re
import logging
import json
import math
import os

# Import MQTT
import paho.mqtt.client as mqtt

# Send Data to MQTT Broker
def Publish_MQTT_Data(Topic, Data, Qos, Retain):
    # Configure MQTT Broker Address, Port and Login Credentials
    Mqtt_Broker_Address = '192.168.1.10'
    Mqtt_Port = 1883
    Mqtt_User = "mqttuser"
    Mqtt_Password = "mqttpassword"

    # Verbindung zum MQTT Broker herstellen
    Mqtt_Client = mqtt.Client()
    Mqtt_Client.username_pw_set(Mqtt_User, Mqtt_Password)
    
    try:
        Mqtt_Client.connect(Mqtt_Broker_Address, Mqtt_Port)
    except Exception as e:
        log("Connection Error while trying to connect to MQTT Broker", e.message)
        pass

    # Write/Send Data to MQTT Broker
    try:
        Mqtt_Client.publish(topic=Topic, payload=Data, qos=Qos, retain=Retain)
        # Im Protokoll ausgeben
        log('Published', str(Topic) + ': ' + str(Data))
    except:
        log("Can't publish MQTT Message", str(Topic) + ',' + str(Data))

    # Close Connection to MQTT Broker
    Mqtt_Client.disconnect()

def log(what,why):
    logging.info(what + ": " + why)

def logTelegram(what,t):
    logging.debug(what + ": " + ' '.join(format(x, '02x') for x in t))

class logamatic2107 (c3964r.Dust3964r,threading.Thread):
    message = True
    timeout = False

    # In Home Assistant muss für jeden Eintrag in protocol = { 0x8000: ...) in der Configuration.yaml ein Sensor unterhalb von mqtt: angelegt werden! 
    # Beispiel:
    # mqtt:
    #   sensor:
    #     - name: "Außentemperatur (Heizung)"
    #       state_topic: "stat/logamatic/Aussentemperatur" # Dieser Wert muss übereinstimmen, der in protocol nach 0x893c: angegeben wurde!
    #       icon: mdi:thermometer
    #       state_class: measurement
    #       device_class: temperature
    #       unit_of_measurement: "°C"

    # protocol 2107 via KM271 
    # Dictonary, das die MQTT Topic-Pfade speichert
    # Die Topic-Pfade können nach stat/ selbst definiert werden
    protocol = {
        0x8000: '',                                             # Betriebswerte 1 HK1
    	0x8001: '',                                             # Betriebswerte 2 HK1
    	0x8002: '',                                             # Vorlaufsolltemperatur HK1
    	0x8003: '',                                             # Vorlaufisttemperatur HK1
    	0x8004: '',                                             # Raumsolltemperatur HK1
    	0x8005: '',                                             # Raumisttemperatur HK1
    	0x8006: '',                                             # Einschaltoptimierungszeit HK1
    	0x8007: '',                                             # Ausschaltoptimierungszeit HK1
    	0x8008: '',                                             # Pumpenleistung HK1
    	0x8009: '',                                             # Mischerstellung HK1
    	0x800a: 'nicht belegt',
    	0x800b: 'nicht belegt',
    	0x800c: '',                                             # Heizkennlinie HK1 bei + 10 Grad
    	0x800d: '',                                             # Heizkennlinie HK1 bei 0 Grad
    	0x800e: '',                                             # Heizkennlinie HK1 bei - 10 Grad
    	0x800f: 'nicht belegt',
    	0x8010: 'nicht belegt',
    	0x8011: 'nicht belegt',
    	0x8112: 'stat/logamatic/Betriebswerte_HK2_1',           # Betriebswerte 1 HK2
    	0x8113: 'stat/logamatic/Betriebswerte_HK2_2',           # Betriebswerte 2 HK2
    	0x8114: 'stat/logamatic/Vorlauftemperatur_HK2_Soll',    # Vorlaufsolltemperatur HK2
    	0x8115: 'stat/logamatic/Vorlauftemperatur_HK2_Ist',     # Vorlaufisttemperatur HK2
    	0x8116: 'stat/logamatic/Raumtemperatur_HK2_Soll',       # Raumsolltemperatur HK2
    	0x8117: 'stat/logamatic/Raumtemperatur_HK2_Ist',        # Raumisttemperatur HK2
    	0x8118: 'stat/logamatic/Einschaltoptimierungszeit_HK2', # Einschaltoptimierungszeit HK2
    	0x8119: 'stat/logamatic/Ausschaltoptimierungszeit_HK2', # Ausschaltoptimierungszeit HK2
    	0x811a: 'stat/logamatic/Pumpenleistung_HK2',            # Pumpenleistung HK2
    	0x811b: 'stat/logamatic/Mischerstellung_HK2',           # Mischerstellung HK2
    	0x811c: 'nicht belegt',
    	0x811d: 'nicht belegt',
    	0x811e: 'stat/logamatic/Heizkennlinie_hk2_10',          # Heizkennlinie HK2 bei + 10 Grad
    	0x811f: 'stat/logamatic/Heizkennlinie_hk2_0',           # Heizkennlinie HK2 bei 0 Grad
    	0x8120: 'stat/logamatic/Heizkennlinie_hk2_-10',         # Heizkennlinie HK2 bei - 10 Grad
    	0x8121: 'nicht belegt',
    	0x8122: 'nicht belegt',
    	0x8123: 'nicht belegt',
    	0x8424: 'stat/logamatic/Betriebswerte_WW_1',            # Betriebswerte 1 WW
    	0x8425: 'stat/logamatic/Betriebswerte_WW_2',            # Betriebswerte 2 WW
    	0x8426: 'stat/logamatic/Warmwassertemperatur_Soll',     # Warmwassersolltemperatur
    	0x8427: 'stat/logamatic/Warmwassertemperatur_Ist',      # Warmwasseristtemperatur
    	0x8428: 'stat/logamatic/Warmwasseroptimierungszeit',    # Warmwasseroptimierungszeit
    	0x8429: 'stat/logamatic/Ladepumpe',                     # Ladepumpe
    	0x882a: 'stat/logamatic/Kesselvorlauftemperatur_soll',  # Kesselvorlaufsolltemperatur
    	0x882b: 'stat/logamatic/Kesselvorlauftemperatur_ist',   # Kesselvorlaufisttemperatur
    	0x882c: 'stat/logamatic/Brennereinschalttemperatur',    # Brennereinschalttemperatur
    	0x882d: 'stat/logamatic/Brennerausschalttemperatur',    # Brennerausschalttemperatur
    	0x882e: '',                                             # Kesselintegral 1
    	0x882f: '',                                             # Kesselintegral 2
    	0x8830: 'stat/logamatic/Kesselfehler',                  # Kesselfehler
    	0x8831: 'stat/logamatic/Kesselbetrieb',                 # Kesselbetrieb
    	0x8832: 'stat/logamatic/Brenneransteuerung',            # Brenneransteuerung
    	0x8833: '',                                             # Abgastemperatur
    	0x8834: '',                                             # modulare Brenner Stellwert
    	0x8835: 'nicht belegt',
    	0x8836: 'stat/logamatic/Brennerlaufzeit_1_Stunden_2',   # Brennerlaufzeit 1 Stunden 2
    	0x8837: 'stat/logamatic/Brennerlaufzeit_1_Stunden_1',   # Brennerlaufzeit 1 Stunden 1
    	0x8838: 'stat/logamatic/Brennerlaufzeit_1_Stunden_0',   # Brennerlaufzeit 1 Stunden 0
    	0x8839: 'stat/logamatic/Brennerlaufzeit_2_Stunden_2',   # Brennerlaufzeit 2 Stunden 2
    	0x883a: 'stat/logamatic/Brennerlaufzeit_2_Stunden_1',   # Brennerlaufzeit 2 Stunden 1
    	0x883b: 'stat/logamatic/Brennerlaufzeit_2_Stunden_0',   # Brennerlaufzeit 2 Stunden 0
    	0x893c: 'stat/logamatic/Aussentemperatur',              # Aussentemperatur    
    	0x893d: '',                                             # gedaempfte Aussentemperatur
    	0x893e: 'stat/logamatic/Versionsnummer_VK',             # Versionsnummer VK
    	0x893f: 'stat/logamatic/Versionsnummer_NK',             # Versionsnummer NK
    	0x8940: 'stat/logamatic/Modulkennung',                  # Modulkennung
    	0x8941: 'nicht belegt'
    }

    #Dictionary, dass zum Speichern der alten Werte dient
    oldValues = {
        0x8000: '', # Betriebswerte 1 HK1
    	0x8001: '', # Betriebswerte 2 HK1
    	0x8002: '', # Vorlaufsolltemperatur HK1
    	0x8003: '', # Vorlaufisttemperatur HK1
    	0x8004: '', # Raumsolltemperatur HK1
    	0x8005: '', # Raumisttemperatur HK1
    	0x8006: '', # Einschaltoptimierungszeit HK1
    	0x8007: '', # Ausschaltoptimierungszeit HK1
     	0x8008: '', # Pumpenleistung HK1
    	0x8009: '', # Mischerstellung HK1
    	0x800a: '', # nicht belegt
    	0x800b: '', # nicht belegt
    	0x800c: '', # Heizkennlinie HK1 bei + 10 Grad
    	0x800d: '', # Heizkennlinie HK1 bei 0 Grad
    	0x800e: '', # Heizkennlinie HK1 bei - 10 Grad
        0x800f: '', # nicht belegt
    	0x8010: '', # nicht belegt
    	0x8011: '', # nicht belegt
     	0x8112: '', # Betriebswerte 1 HK2
    	0x8113: '', # Betriebswerte 2 HK2
        0x8114: '', # Vorlaufsolltemperatur HK2
    	0x8115: '', # Vorlaufisttemperatur HK2
        0x8116: '', # Raumsolltemperatur HK2
    	0x8117: '', # Raumisttemperatur HK2
        0x8118: '', # Einschaltoptimierungszeit HK2
    	0x8119: '', # Ausschaltoptimierungszeit HK2
        0x811a: '', # Pumpenleistung HK2
    	0x811b: '', # Mischerstellung HK2
        0x811c: '', # nicht belegt
    	0x811d: '', # nicht belegt
        0x811e: '', # Heizkennlinie HK2 bei + 10 Grad
    	0x811f: '', # Heizkennlinie HK2 bei 0 Grad
    	0x8120: '', # Heizkennlinie HK2 bei - 10 Grad
        0x8121: '', # nicht belegt
    	0x8122: '', # nicht belegt
    	0x8123: '', # nicht belegt
        0x8424: '', # Betriebswerte 1 WW
    	0x8425: '', # Betriebswerte 2 WW
        0x8426: '', # Warmwassersolltemperatur
    	0x8427: '', # Warmwasseristtemperatur
    	0x8428: '', # Warmwasseroptimierungszeit
    	0x8429: '', # Ladepumpe
        0x882a: '', # Kesselvorlaufsolltemperatur
    	0x882b: '', # Kesselvorlaufisttemperatur
        0x882c: '', # Brennereinschalttemperatur
    	0x882d: '', # Brennerausschalttemperatur
        0x882e: '', # Kesselintegral 1
    	0x882f: '', # Kesselintegral 2
        0x8830: '', # Kesselfehler
    	0x8831: '', # Kesselbetrieb
        0x8832: '', # Brenneransteuerung
        0x8833: '', # Abgastemperatur
    	0x8834: '', # modulare Brenner Stellwert
    	0x8835: '', # nicht belegt
        0x8836: '', # Brennerlaufzeit 1 Stunden 2
    	0x8837: '', # Brennerlaufzeit 1 Stunden 1
    	0x8838: '', # Brennerlaufzeit 1 Stunden 0
    	0x8839: '', # Brennerlaufzeit 2 Stunden 2
    	0x883a: '', # Brennerlaufzeit 2 Stunden 1
    	0x883b: '', # Brennerlaufzeit 2 Stunden 0
        0x893c: '', # Aussentemperatur    
    	0x893d: '', # gedaempfte Aussentemperatur
        0x893e: '', # Versionsnummer VK
    	0x893f: '', # Versionsnummer NK
    	0x8940: '', # Modulkennung
    	0x8941: '', # nicht belegt
    }

    # Installations Konstruktor für den Treiber und für die Threads
    def __init__ (self):
        self.timeout = False
        c3964r.Dust3964r.__init__ (self,port='COM10',baudrate=2400,SLP=0.1, PRIO=self.HIPRIO)
        threading.Thread.__init__ (self)
        self.TimeStampMessage= t.time()
    
    # Das hier ist die eigentliche Routine die vom Thread aufgerufen wird
    def run (self):
        global ende
        while not ende:
            t.sleep (0.1)
            # Test auf Message TimeOut
            self.timeout= (t.time () - self.TimeStampMessage)>20
            self.running ()
        #log ("Run", "Logamatic Thread closed")     

    # WriteSucces wird aufgerufen, wenn ein Schreibbefehl über den 3964r erfolgreich an die Logamatic abgesetzt wurde
    def WriteSuccess (self,telegram):
        logTelegram ("Write Success",telegram)
        if self.message:
            # Hier analysieren wir für den Debugger das Telegram
            if telegram==b"\xEE\x00\x00": # Logmode.
                #log ("Write Success","CMD: LOGMODE eingeleitet")
                pass

    def WriteFail (self,telegram):
        #log ("error","SENDEJOB NICHT ERFOLGREICH, ABGEBROCHEN")
        #logging.error ("SENDEJOB %s NICHT ERFOLGREICH, ABGEBROCHEN", telegram)       
        pass
    
    # Read Success wird vom child aufgerufen, wenn Telegramm erfolgreich von der Logamatik gelesen wurde
    def ReadSuccess (self,telegram):
        # entweder dekodierte Ausgabe oder das ursprüngliche Telegram
        # 3 Byte Botschaften kennen wir (normalerweise) als protocol
        if len(telegram) == 3:
            # Key und Value ermitteln und in Variablen speichern
            key = telegram[0]*256 + telegram[1]
            value = telegram[2]

            # Ist der Key bekannt, siehe Dictonary protocol
            if key in self.protocol:
                # Nicht belegte Keys oder Einträge ohne MQTT Topics sollen ausgelassen werden
                if self.protocol[key] != 'nicht belegt' and self.protocol[key] != '':

                    # Aussentemperatur
                    if key == 0x893c: 
                        # Aussentemperatur kann negativ werden -> value - 256
                        if value > 128:
                            value = value - 256
                        else:
                            value = value

                    # Prüfen, ob der aktuelle Wert sich vom alten Wert unterscheidet
                    if value != self.oldValues[key]:
                        # Nur wenn der aktuelle Wert sich geändert hat, eine MQTT Publish Nachricht versenden
                        Publish_MQTT_Data(Topic=self.protocol[key],Data=value,Qos=1,Retain=True)
                        
                        # Neuen Wert zwischenspeichern
                        self.oldValues[key] = value
                        pass
                    else:
                        #log('Keine Änderung', str(self.protocol[key]) + ': ' + str(self.oldValues[key]) + ' -> ' + str(value))
                        pass 
                else:
                    #log('Key ' + str(hex(key)) + ' ist nicht belegt und wird daher übersprungen...', 'Nicht belegter Key')
                    pass
            else:
                #logTelegram ("Read Success",telegram)
                #log("Read Success","Unknown key")
                pass
            
        elif len(telegram) == 8 and telegram == [4,0,7,1,193,202,128,141]:        # Hex:  04 00 07 01 c1 ca 80 8d
            # skip - seems to be the message "nothing happened"
            #log("","")
            pass
        else:
            #logTelegram ("Read Success",telegram)
            pass

# configure logging to file
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%d.%m.%Y %H:%M:%S ', level=logging.INFO, filename='C:\Messwerte\Heizung\logamatic.log')

ende=False
a=logamatic2107 ()
a.CFG_PRINT=False  # Einzeltelegramm Hex DEBUG Aus
a.newJob (b"\xee\x00\x00")
a.RealRun= True
a.start ()

try:
    while not ende:
        t.sleep (0.1)
except (KeyboardInterrupt,SystemExit):
    ende=True
    pass

t.sleep (4)
log ("Main","Programmende")

In configuration.yaml a lot of sensors have to defined.
Not all are really needed. It depends on your configuration.

mqtt:
  sensor:
    # Buderus Heizung =====================================
    - name: "Außentemperatur (Heizung)"
      state_topic: "stat/logamatic/Aussentemperatur"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Außentemperatur (Heizung) (gedämpft)"
      state_topic: "stat/logamatic/Aussentemperatur_gedämpft"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur Kessel (Ist)"
      state_topic: "stat/logamatic/Kesselvorlauftemperatur_ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur Kessel (Soll)"
      state_topic: "stat/logamatic/Kesselvorlauftemperatur_soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur HK1 (Ist)"
      state_topic: "stat/logamatic/Vorlauftemperatur_HK1_Ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur HK1 (Soll)"
      state_topic: "stat/logamatic/Vorlauftemperatur_HK1_Soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur HK2 (Ist)"
      state_topic: "stat/logamatic/Vorlauftemperatur_HK2_Ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Vorlauftemperatur HK2 (Soll)"
      state_topic: "stat/logamatic/Vorlauftemperatur_HK2_Soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Warmwassertemperatur (Ist)"
      state_topic: "stat/logamatic/Warmwassertemperatur_Ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Warmwassertemperatur (Soll)"
      state_topic: "stat/logamatic/Warmwassertemperatur_Soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Warmwasseroptimierungszeit"
      state_topic: "stat/logamatic/Warmwasseroptimierungszeit"
      state_class: measurement

    - name: "Ladepumpe"
      state_topic: "stat/logamatic/Ladepumpe"
      state_class: measurement

    - name: "Brennerlaufzeit 1 Stunden 0"
      state_topic: "stat/logamatic/Brennerlaufzeit_1_Stunden_0"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Brennerlaufzeit 1 Stunden 1"
      state_topic: "stat/logamatic/Brennerlaufzeit_1_Stunden_1"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Brennerlaufzeit 1 Stunden 2"
      state_topic: "stat/logamatic/Brennerlaufzeit_1_Stunden_2"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Brennerlaufzeit 2 Stunden 0"
      state_topic: "stat/logamatic/Brennerlaufzeit_2_Stunden_0"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Brennerlaufzeit 2 Stunden 1"
      state_topic: "stat/logamatic/Brennerlaufzeit_2_Stunden_1"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Brennerlaufzeit 2 Stunden 2"
      state_topic: "stat/logamatic/Brennerlaufzeit_2_Stunden_2"
      state_class: measurement
      unit_of_measurement: "h"
      icon: mdi:clock
      device_class: duration

    - name: "Betriebswerte 1 HK1"
      state_topic: "stat/logamatic/Betriebswerte_HK1_1"
      state_class: measurement

    - name: "Betriebswerte 2 HK1"
      state_topic: "stat/logamatic/Betriebswerte_HK1_2"
      state_class: measurement

    - name: "Betriebswerte 1 HK2"
      state_topic: "stat/logamatic/Betriebswerte_HK2_1"
      state_class: measurement

    - name: "Betriebswerte 2 HK2"
      state_topic: "stat/logamatic/Betriebswerte_HK2_2"
      state_class: measurement

    - name: "Betriebswerte 1 WW"
      state_topic: "stat/logamatic/Betriebswerte_WW_1"
      state_class: measurement

    - name: "Betriebswerte 2 WW"
      state_topic: "stat/logamatic/Betriebswerte_WW_2"
      state_class: measurement

    - name: "Raumtemperatur HK1 (Ist)"
      state_topic: "stat/logamatic/Raumtemperatur_HK1_Ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Raumtemperatur HK1 (Soll)"
      state_topic: "stat/logamatic/Raumtemperatur_HK1_Soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Raumtemperatur HK2 (Ist)"
      state_topic: "stat/logamatic/Raumtemperatur_HK2_Ist"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Raumtemperatur HK2 (Soll)"
      state_topic: "stat/logamatic/Raumtemperatur_HK2_Soll"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Pumpenleistung HK1"
      state_topic: "stat/logamatic/Pumpenleistung_HK1"
      state_class: measurement
      device_class: power_factor
      icon: mdi:pump

    - name: "Pumpenleistung HK2"
      state_topic: "stat/logamatic/Pumpenleistung_HK2"
      state_class: measurement
      device_class: power_factor
      icon: mdi:pump

    - name: "Mischerstellung HK1"
      state_topic: "stat/logamatic/Mischerstellung_HK1"
      state_class: measurement
      icon: mdi:valve

    - name: "Mischerstellung HK2"
      state_topic: "stat/logamatic/Mischerstellung_HK2"
      state_class: measurement
      icon: mdi:valve

    - name: "Heizkennlinie HK1 +10°"
      state_topic: "stat/logamatic/Heizkennlinie_hk1_10"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Heizkennlinie HK1 +0°"
      state_topic: "stat/logamatic/Heizkennlinie_hk1_0"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Heizkennlinie HK1 -10°"
      state_topic: "stat/logamatic/Heizkennlinie_hk1_-10"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Heizkennlinie HK2 +10°"
      state_topic: "stat/logamatic/Heizkennlinie_hk2_10"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Heizkennlinie HK2 +0°"
      state_topic: "stat/logamatic/Heizkennlinie_hk2_0"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Heizkennlinie HK2 -10°"
      state_topic: "stat/logamatic/Heizkennlinie_hk2_-10"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Kesselintegral 1"
      state_topic: "stat/logamatic/Kesselintegral_1"
      state_class: measurement

    - name: "Kesselintegral 2"
      state_topic: "stat/logamatic/Kesselintegral_2"
      state_class: measurement

    - name: "Einschaltoptimierungszeit HK1"
      state_topic: "stat/logamatic/Einschaltoptimierungszeit_HK1"
      state_class: measurement

    - name: "Ausschaltoptimierungszeit HK1"
      state_topic: "stat/logamatic/Ausschaltoptimierungszeit_HK1"
      state_class: measurement

    - name: "Einschaltoptimierungszeit HK2"
      state_topic: "stat/logamatic/Einschaltoptimierungszeit_HK2"
      state_class: measurement

    - name: "Ausschaltoptimierungszeit HK2"
      state_topic: "stat/logamatic/Ausschaltoptimierungszeit_HK2"
      state_class: measurement

    - name: "Brennereinschalttemperatur"
      state_topic: "stat/logamatic/Brennereinschalttemperatur"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Brennerausschalttemperatur"
      state_topic: "stat/logamatic/Brennerausschalttemperatur"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Kesselfehler"
      state_topic: "stat/logamatic/Kesselfehler"
      state_class: measurement
      icon: mdi:alert

    - name: "Kesselbetrieb"
      state_topic: "stat/logamatic/Kesselbetrieb"
      state_class: measurement

    - name: "Brenneransteuerung"
      state_topic: "stat/logamatic/Brenneransteuerung"
      state_class: measurement

    - name: "Abgastemperatur"
      state_topic: "stat/logamatic/Abgastemperatur"
      icon: mdi:thermometer
      state_class: measurement
      device_class: temperature
      unit_of_measurement: "°C"

    - name: "Modularer Brenner Stellwert"
      state_topic: "stat/logamatic/ModularerBrennerStellwert"
      state_class: measurement

    - name: "Modulkennung"
      state_topic: "stat/logamatic/Modulkennung"
      state_class: measurement

    - name: "Versionsnummer VK"
      state_topic: "stat/logamatic/Versionsnummer_VK"
      state_class: measurement

    - name: "Versionsnummer NK"
      state_topic: muss in
      state_class: measurement

Here my Heating page

and warm water page

I had a strange Problem.

I have set up a helper on a daily basis that measures the time during which the oil burner is switched on. Therefor I used the total running time.

unerwünschter Sprung beim Übertrag

The fall to zero at 0 o’clock is wanted and ok, but how can it be that within one or a few minutes more than 200 minutes are added?

This can not be!

It was a little bit tricky to figure out the reason.

The time during which the oil burner is switched on is devided into 3 separate c3964r telegrams.

Brennerlaufzeit_1_2 which stands for 2562 = 65536 Minutes
Brennerlaufzeit_1_1 which stands for 2561 = 256 Minutes
Brennerlaufzeit_1_0 which stands for 2560 = 1 Minute

All 3 values added ​​result in the total running time.

Because the KM 271 delivers first the msb (most significant bit) and the lsb (least signicficant bit) at last sent, a leap of 256 occurs and the 255 from the lsb is actually is active until the lsb is sent to the mqtt broker.

For a second or less, the 255 from the lsb is still active. I would actually think that after the lsb is sent the curve would fall downwards, but that doesn’t happen and it stays at the top. The faulty sum was not corrected by HA.

The phenomenon can also occur at the bit shift at 65536.

So I have to rewrite my python script and reconfigure my HA instance, so that first all 3 times (minutes) added to one value and sent as one mqtt message.


#!/usr/bin/python3
# -*-coding:Utf-8 -*
# Credits to:  http://foto-paintings.de/index.php/hausautomatisierung/12-heizung/16-test
#         and  https://homematic-forum.de/forum/viewtopic.php?f=18&t=26955&start=40

# Ergänzt am 02.01.2024 von Janihani für MQTT

from datetime import datetime
from urllib.request import urlopen
from http.server import BaseHTTPRequestHandler, HTTPServer
import time as t
import binascii
import c3964r
import threading
import http.client
import socket
import re
import logging
import json
import math
import os

# Import MQTT
import paho.mqtt.client as mqtt

# In welcher Datei soll geloggt werden?
LogFileName = 'C:\Messwerte\Heizung\logamatic.log'

# An welchem Comport ist das KM271 Kommunikationsmodul angeschlossen?
COMPORT = 'COM10' 

# Herausfinden, an welchem Comport was angeschlossen ist
#python -m serial.tools.miniterm

#--- Available ports:
#---  1: COM5                 'Intel(R) Active Management Technology - SOL (COM5)'
#---  2: COM6                 'Silicon Labs CP210x USB to UART Bridge (COM6)'
#---  3: COM9                 'USB-SERIAL CH341A (COM9)'
#---  4: COM10                'Prolific USB-to-Serial Comm Port (COM10)'

# Send Data to MQTT Broker
def Publish_MQTT_Data(Topic, Data, Qos, Retain):
    # Configure MQTT Broker Address, Port and Login Credentials
    Mqtt_Broker_Address = '192.168.1.1'
    Mqtt_Port = 1883
    Mqtt_User = "mqttuser"
    Mqtt_Password = "mqttpassword"

    # Verbindung zum MQTT Broker herstellen
    Mqtt_Client = mqtt.Client()
    Mqtt_Client.username_pw_set(Mqtt_User, Mqtt_Password)
    
    try:
        Mqtt_Client.connect(Mqtt_Broker_Address, Mqtt_Port)
    except Exception as e:
        log("Connection Error while trying to connect to MQTT Broker", e)
        pass

    # Write/Send Data to MQTT Broker
    try:
        Mqtt_Client.publish(topic=Topic, payload=Data, qos=Qos, retain=Retain)
        # Im Protokoll ausgeben
        log('Published', str(Topic) + ': ' + str(Data))
    except Exception as e:
        log("Can't publish MQTT Message to " + str(Topic) + ', ' + str(Data), e)

    # Close Connection to MQTT Broker
    Mqtt_Client.disconnect()

def log(what,why):
    logging.info(what + ": " + why)

def logTelegram(what,t):
    logging.debug(what + ": " + ' '.join(format(x, '02x') for x in t))

class logamatic2107 (c3964r.Dust3964r,threading.Thread):
    message = True
    timeout = False

    # In Home Assistant muss für jeden Eintrag in protocol = { 0x8000: ...) in der Configuration.yaml ein Sensor unterhalb von mqtt: angelegt werden! 
    # Wichtig, auf die Einrückungen achten!
    # Beispiel:
    # mqtt:
    #   sensor:
    #     - name: "Außentemperatur (Heizung)"
    #       state_topic: "stat/logamatic/Aussentemperatur" # Dieser Wert muss übereinstimmen, der in protocol nach 0x893c: angegeben wurde!
    #       icon: mdi:thermometer
    #       state_class: measurement
    #       device_class: temperature
    #       unit_of_measurement: "°C"

    # protocol 2107 via KM271 
    # Dictonary, das die MQTT Topic-Pfade speichert
    # Die Topic-Pfade können nach stat/ selbst definiert werden
 
    protocol = {
        0x8000: '',                                             # Betriebswerte 1 HK1
    	0x8001: '',                                             # Betriebswerte 2 HK1
    	0x8002: '',                                             # Vorlaufsolltemperatur HK1
    	0x8003: '',                                             # Vorlaufisttemperatur HK1
    	0x8004: '',                                             # Raumsolltemperatur HK1
    	0x8005: '',                                             # Raumisttemperatur HK1
    	0x8006: '',                                             # Einschaltoptimierungszeit HK1
    	0x8007: '',                                             # Ausschaltoptimierungszeit HK1
    	0x8008: '',                                             # Pumpenleistung HK1
    	0x8009: '',                                             # Mischerstellung HK1
    	0x800a: 'nicht belegt',
    	0x800b: 'nicht belegt',
    	0x800c: '',                                             # Heizkennlinie HK1 bei + 10 Grad
    	0x800d: '',                                             # Heizkennlinie HK1 bei 0 Grad
    	0x800e: '',                                             # Heizkennlinie HK1 bei - 10 Grad
    	0x800f: 'nicht belegt',
    	0x8010: 'nicht belegt',
    	0x8011: 'nicht belegt',
    	0x8112: 'stat/logamatic/Betriebswerte_HK2_1',           # Betriebswerte 1 HK2
    	0x8113: 'stat/logamatic/Betriebswerte_HK2_2',           # Betriebswerte 2 HK2
    	0x8114: 'stat/logamatic/Vorlauftemperatur_HK2_Soll',    # Vorlaufsolltemperatur HK2
    	0x8115: 'stat/logamatic/Vorlauftemperatur_HK2_Ist',     # Vorlaufisttemperatur HK2
    	0x8116: 'stat/logamatic/Raumtemperatur_HK2_Soll',       # Raumsolltemperatur HK2
    	0x8117: 'stat/logamatic/Raumtemperatur_HK2_Ist',        # Raumisttemperatur HK2
    	0x8118: 'stat/logamatic/Einschaltoptimierungszeit_HK2', # Einschaltoptimierungszeit HK2
    	0x8119: 'stat/logamatic/Ausschaltoptimierungszeit_HK2', # Ausschaltoptimierungszeit HK2
    	0x811a: 'stat/logamatic/Pumpenleistung_HK2',            # Pumpenleistung HK2
    	0x811b: 'stat/logamatic/Mischerstellung_HK2',           # Mischerstellung HK2
    	0x811c: 'nicht belegt',
    	0x811d: 'nicht belegt',
    	0x811e: 'stat/logamatic/Heizkennlinie_hk2_10',          # Heizkennlinie HK2 bei + 10 Grad
    	0x811f: 'stat/logamatic/Heizkennlinie_hk2_0',           # Heizkennlinie HK2 bei 0 Grad
    	0x8120: 'stat/logamatic/Heizkennlinie_hk2_-10',         # Heizkennlinie HK2 bei - 10 Grad
    	0x8121: 'nicht belegt',
    	0x8122: 'nicht belegt',
    	0x8123: 'nicht belegt',
    	0x8424: 'stat/logamatic/Betriebswerte_WW_1',            # Betriebswerte 1 WW
    	0x8425: 'stat/logamatic/Betriebswerte_WW_2',            # Betriebswerte 2 WW
    	0x8426: 'stat/logamatic/Warmwassertemperatur_Soll',     # Warmwassersolltemperatur
    	0x8427: 'stat/logamatic/Warmwassertemperatur_Ist',      # Warmwasseristtemperatur
    	0x8428: 'stat/logamatic/Warmwasseroptimierungszeit',    # Warmwasseroptimierungszeit
    	0x8429: 'stat/logamatic/Ladepumpe',                     # Ladepumpe
    	0x882a: 'stat/logamatic/Kesselvorlauftemperatur_soll',  # Kesselvorlaufsolltemperatur
    	0x882b: 'stat/logamatic/Kesselvorlauftemperatur_ist',   # Kesselvorlaufisttemperatur
    	0x882c: 'stat/logamatic/Brennereinschalttemperatur',    # Brennereinschalttemperatur
    	0x882d: 'stat/logamatic/Brennerausschalttemperatur',    # Brennerausschalttemperatur
    	0x882e: '',                                             # Kesselintegral 1
    	0x882f: '',                                             # Kesselintegral 2
    	0x8830: 'stat/logamatic/Kesselfehler',                  # Kesselfehler
    	0x8831: 'stat/logamatic/Kesselbetrieb',                 # Kesselbetrieb
    	0x8832: 'stat/logamatic/Brenneransteuerung',            # Brenneransteuerung
    	0x8833: '',                                             # Abgastemperatur
    	0x8834: '',                                             # modulare Brenner Stellwert
    	0x8835: 'nicht belegt',
    	0x8836: '',                                             # Brennerlaufzeit 1 Stunden 2
    	0x8837: '',                                             # Brennerlaufzeit 1 Stunden 1
    	0x8838: 'stat/logamatic/Brennerlaufzeit_Minuten',       # Brennerlaufzeit 1 Stunden 0 (Gesamtminuten, aus _2 + _1 + _0 berechnet)
    	0x8839: '',                                             # Brennerlaufzeit 2 Stunden 2
    	0x883a: '',                                             # Brennerlaufzeit 2 Stunden 1
    	0x883b: '',                                             # Brennerlaufzeit 2 Stunden 0
    	0x893c: 'stat/logamatic/Aussentemperatur',              # Aussentemperatur    
    	0x893d: '',                                             # gedaempfte Aussentemperatur
    	0x893e: 'stat/logamatic/Versionsnummer_VK',             # Versionsnummer VK
    	0x893f: 'stat/logamatic/Versionsnummer_NK',             # Versionsnummer NK
    	0x8940: 'stat/logamatic/Modulkennung',                  # Modulkennung
    	0x8941: 'nicht belegt'
    }

    #Dictionary, dass zum Speichern der alten Werte dient
    oldValues = {
        0x8000: '', # Betriebswerte 1 HK1
    	0x8001: '', # Betriebswerte 2 HK1
    	0x8002: '', # Vorlaufsolltemperatur HK1
    	0x8003: '', # Vorlaufisttemperatur HK1
    	0x8004: '', # Raumsolltemperatur HK1
    	0x8005: '', # Raumisttemperatur HK1
    	0x8006: '', # Einschaltoptimierungszeit HK1
    	0x8007: '', # Ausschaltoptimierungszeit HK1
     	0x8008: '', # Pumpenleistung HK1
    	0x8009: '', # Mischerstellung HK1
    	0x800a: '', # nicht belegt
    	0x800b: '', # nicht belegt
    	0x800c: '', # Heizkennlinie HK1 bei + 10 Grad
    	0x800d: '', # Heizkennlinie HK1 bei 0 Grad
    	0x800e: '', # Heizkennlinie HK1 bei - 10 Grad
        0x800f: '', # nicht belegt
    	0x8010: '', # nicht belegt
    	0x8011: '', # nicht belegt
     	0x8112: '', # Betriebswerte 1 HK2
    	0x8113: '', # Betriebswerte 2 HK2
        0x8114: '', # Vorlaufsolltemperatur HK2
    	0x8115: '', # Vorlaufisttemperatur HK2
        0x8116: '', # Raumsolltemperatur HK2
    	0x8117: '', # Raumisttemperatur HK2
        0x8118: '', # Einschaltoptimierungszeit HK2
    	0x8119: '', # Ausschaltoptimierungszeit HK2
        0x811a: '', # Pumpenleistung HK2
    	0x811b: '', # Mischerstellung HK2
        0x811c: '', # nicht belegt
    	0x811d: '', # nicht belegt
        0x811e: '', # Heizkennlinie HK2 bei + 10 Grad
    	0x811f: '', # Heizkennlinie HK2 bei 0 Grad
    	0x8120: '', # Heizkennlinie HK2 bei - 10 Grad
        0x8121: '', # nicht belegt
    	0x8122: '', # nicht belegt
    	0x8123: '', # nicht belegt
        0x8424: '', # Betriebswerte 1 WW
    	0x8425: '', # Betriebswerte 2 WW
        0x8426: '', # Warmwassersolltemperatur
    	0x8427: '', # Warmwasseristtemperatur
    	0x8428: '', # Warmwasseroptimierungszeit
    	0x8429: '', # Ladepumpe
        0x882a: '', # Kesselvorlaufsolltemperatur
    	0x882b: '', # Kesselvorlaufisttemperatur
        0x882c: '', # Brennereinschalttemperatur
    	0x882d: '', # Brennerausschalttemperatur
        0x882e: '', # Kesselintegral 1
    	0x882f: '', # Kesselintegral 2
        0x8830: '', # Kesselfehler
    	0x8831: '', # Kesselbetrieb
        0x8832: '', # Brenneransteuerung
        0x8833: '', # Abgastemperatur
    	0x8834: '', # modulare Brenner Stellwert
    	0x8835: '', # nicht belegt
        0x8836: '', # Brennerlaufzeit 1 Stunden 2
    	0x8837: '', # Brennerlaufzeit 1 Stunden 1
    	0x8838: '', # Brennerlaufzeit 1 Stunden 0
    	0x8839: '', # Brennerlaufzeit 2 Stunden 2
    	0x883a: '', # Brennerlaufzeit 2 Stunden 1
    	0x883b: '', # Brennerlaufzeit 2 Stunden 0
        0x893c: '', # Aussentemperatur    
    	0x893d: '', # gedaempfte Aussentemperatur
        0x893e: '', # Versionsnummer VK
    	0x893f: '', # Versionsnummer NK
    	0x8940: '', # Modulkennung
    	0x8941: '', # nicht belegt
    }

    # Installations Konstruktor für den Treiber und für die Threads
    def __init__ (self):
        self.timeout = False
        c3964r.Dust3964r.__init__ (self,port=COMPORT,baudrate=2400,SLP=0.1, PRIO=self.HIPRIO)
        threading.Thread.__init__ (self)
        self.TimeStampMessage= t.time()
    
    # Das hier ist die eigentliche Routine die vom Thread aufgerufen wird
    def run (self):
        global ende
        while not ende:
            t.sleep (0.1)
            # Test auf Message TimeOut
            self.timeout= (t.time () - self.TimeStampMessage)>20
            self.running ()
        log ("Run", "Logamatic Thread closed")     

    # WriteSucces wird aufgerufen, wenn ein Schreibbefehl über den 3964r erfolgreich an die Logamatic abgesetzt wurde
    def WriteSuccess (self,telegram):
        #logTelegram ("Write Success",telegram)
        if self.message:
            # Hier analysieren wir für den Debugger das Telegram
            if telegram==b"\xEE\x00\x00": 
                log ("b\\xEE\\x00\\x00 an die Logamatic gesendet","Daten lesen wurde initiiert ...")
                pass

    def WriteFail (self,telegram):
        log ("error","SENDEJOB NICHT ERFOLGREICH, ABGEBROCHEN")
        logging.error ("SENDEJOB %s NICHT ERFOLGREICH, ABGEBROCHEN", telegram)       
        pass
    
    # Read Success wird vom child aufgerufen, wenn Telegramm erfolgreich von der Logamatik gelesen wurde
    def ReadSuccess (self,telegram):
        # entweder dekodierte Ausgabe oder das ursprüngliche Telegram
        # 3 Byte Botschaften kennen wir (normalerweise) als protocol
        if len(telegram) == 3:
            # Key und Value ermitteln und in Variablen speichern
            key = telegram[0]*256 + telegram[1]
            value = telegram[2]

            # Ist der Key bekannt? Siehe protocol = { } ...
            if key in self.protocol:
                # Nicht belegte Keys oder Einträge ohne MQTT Topics sollen ausgelassen werden
                # Da die meisten Brennerlaufzeiten kein Topic-Inhalt (mehr) haben, müssen die Keys ebenfalls erlaubt sein, 
                # da ansonsten keine Berechnungen der Brennerlaufzeiten stattfinden kann!
                if self.protocol[key] != 'nicht belegt' and self.protocol[key] != '' or key == 0x8836 or key == 0x8837 or key == 0x8838 or key == 0x8839 or key == 0x883a or key == 0x883b:

                    # Aussentemperatur
                    if key == 0x893c: 
                        # Aussentemperatur kann negativ werden -> value - 256
                        if value > 128:
                            value = value - 256
                        else:
                            value = value
                    
                    # Prüfen, ob der aktuelle Wert sich vom alten Wert unterscheidet
                    if value != self.oldValues[key]:
                        # Nur wenn der aktuelle Wert sich geändert hat, eine MQTT Publish Nachricht versenden
                        # Das spart die ein oder andere MQTT Publish Nachricht

                        # Wichtiger Hinweis zu den Brennerlaufzeiten: 
                        # Es gibt 2 Brenner, und die Brennerlaufzeit ist auf 3 Nachrichten aufgeteilt.
                        # Brennerlaufzeit_{1|2}_2, Brennerlaufzeit_{1|2}_1 und Brennerlaufzeit_{1|2}_0.
                        
                        # Brennerlaufzeit_{1|2}_2 hat eine Wertigkeit von 65536 Minuten
                        # Brennerlaufzeit_{1|2}_1 hat eine Wertigkeit von 256 Minuten
                        # Brennerlaufzeit_{1_2}_0 hat eine Wertigkeit von 1 Minute
                          
                        # Zuerst wird die Brennerlaufzeit_{1|2} _2, dann _1 und zuletzt _0 über die Schnittstelle ausgeliefert.

                        # Das führt bei einem Übertrag an kleinerer Stelle dazu, dass ein Sprung von 256 bzw. seltener an größerer Stelle 
                        # ein Sprung von 65536 auftritt, da zuerst die höhere Stellenwertigkeit von der Schnittstelle geliefert und per MQTT
                        # übertragen wird und zuletzt die niederwertigste Wertigkeit, aber HA beim Eintreffen einer MQTT Nachricht "sofort reagiert"
                        # und die Statistik anpasst.

                        # Daher dürfen die Betriebszeiten erst per MQTT gesendet werden, wenn alle 3 Werte von der Schnittstelle geliefert wurden. 
                        # Ansonsten führt das zu unerwünschten Sprüngen (256 oder 65536) in den Statistiken!

                        # Daher ist auch die Zwischenspeicherung der (alten) Werte wichtig, da ja nur jeweils ein Telegram an diese Funktion übergeben wird
                        # und die Zeiten aus 3 Nachrichten bestehen. 

                        if key == 0x8838 and isinstance(self.oldValues[0x8836],int) and isinstance(self.oldValues[0x8837],int):
                            # Brennerlaufzeit_1_0 und die beiden anderen Werte sind Integer und können somit addiert werden
                            # Die Gesamtminuten für Brennerlaufzeit_1 aus den 3 Teilen (_2, _1, _0) zusammensetzen
                            Minuten = int(self.oldValues[0x8836] * 65536) + int(self.oldValues[0x8837] * 256) + int(value)
                            # Falls nur ein Heizkreis vorhanden ist, kann ein MQTT Topic leer sein
                            if self.protocol[key] != '':
                                Publish_MQTT_Data(Topic=self.protocol[key],Data=Minuten,Qos=1,Retain=True)
                            pass
                        elif key == 0x883b and isinstance(self.oldValues[0x8839],int) and isinstance(self.oldValues[0x883a],int):
                            # Brennerlaufzeit_2_0 und die beiden anderen Werte sind Integer und können somit addiert werden
                            # Die Gesamtminuten für Brennerlaufzeit_2 aus den 3 Teilen (_2, _1, _0) zusammensetzen
                            Minuten = int(self.oldValues[0x8839] * 65536) + int(self.oldValues[0x883a] * 256) + int(value)
                            # Falls nur ein Heizkreis vorhanden ist, kann ein MQTT Topic leer sein
                            if self.protocol[key] != '':
                                Publish_MQTT_Data(Topic=self.protocol[key],Data=Minuten,Qos=1,Retain=True)
                            pass
                        elif key == 0x8836 or key == 0x8837 or key == 0x8838 or key == 0x8839 or key == 0x883a or key == 0x883b:
                            # Es ist zwar eine Brennerlaufzeit_{1|2}_{0|1|2} aber die jeweils beiden anderen Werte sind keine Integer
                            # und können damit nicht addiert werden. Das würde zu einer falschen Berechnung/Übermittlung der Brennerlaufzeiten führen.
                            pass
                        else: 
                            # alle anderen Topics 
                            Publish_MQTT_Data(Topic=self.protocol[key],Data=value,Qos=1,Retain=True)
                        
                        # Neuen/Geänderten Wert zwischenspeichern
                        # Ist auch wichtig, damit die Brennerlaufzeiten_{1|2}_{2|1|0} aus den jeweils 3 Bestandteilen addiert werden können,
                        # ansonsten liegt nur ein Wert vor, da an def ReadSuccess (self,telegram) nur ein Telegram, also ein key = value übermittelt wird!
                        self.oldValues[key] = value
                        pass
                    else:
                        #log('Keine Änderung', str(self.protocol[key]) + ': ' + str(self.oldValues[key]) + ' -> ' + str(value))
                        pass 
                else:
                    #log('Key ' + str(hex(key)) + ' ist nicht belegt und wird daher übersprungen...', 'Nicht belegter Key')
                    pass
            else:
                #logTelegram ("Read Success",telegram)
                #log("Read Success","Unknown key")
                pass
            
        elif len(telegram) == 8 and telegram == [4,0,7,1,193,202,128,141]:        # Hex:  04 00 07 01 c1 ca 80 8d
            # skip - seems to be the message "nothing happened"
            #log("","")
            pass
        else:
            #logTelegram ("Read Success",telegram)
            pass

# configure logging to file
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%d.%m.%Y %H:%M:%S ', level=logging.INFO, filename=LogFileName)

ende=False
a=logamatic2107 ()
a.CFG_PRINT=False  # Einzeltelegramm Hex DEBUG Aus
a.newJob (b"\xee\x00\x00")
a.RealRun= True
a.start ()

try:
    while not ende:
        t.sleep (0.1)
except (KeyboardInterrupt,SystemExit):
    ende=True
    pass

t.sleep (4)
log ("Main","Programmende")

There are two more Python scripts are necessary.

c3964r.py:

#!/usr/bin/python3
# -*-coding:Utf-8 -*

# 3964r Protokoll Treiber für RS232
# Michael Thelen OKT 2015

from serial import Serial
from datetime import datetime
import time as t
import binascii
import threading
from schritte import stepchain

#---------------------------------------------------------------------------
# Schrittkette für 3964r Protokoll
# Grundklasse ist Stepchain, elementare Schrittkette
#
# Die Prozedur 3964R ist ein asynchrones, bitserielles Übertragungsverfahren. Über die Verbindung werden
# Steuer- und Nutzinformationszeichen gesendet. Um jedes Zeichen beim Empfänger wiederzuerkennen,
# und um die fehlerfreie Übertragung zu kontrollieren, werden den gesendeten Zeichen weitere Bits voranbzw.
# nachgestellt. Die Reihenfolge der Bits auf der Leitung ist:
# SA I0 I1 I2 I3 I4 I5 I6 I7 SO
# SA = Startbit
# In = Informationsbit Nr.
# SO = Stoppbit
#
# Die Steuerzeichen für die Prozedur 3964R sind der Norm DIN 66003 für den 7-Bit-Code entnommen. Sie
# werden allerdings mit der Zeichenlänge 8 Bit übertragen (Bit I7 = 0). Am Ende jedes Datenblocks wird zur
# Datensicherung ein Prüfzeichen(BCC) gesendet.
# Das Blockprüfzeichen wird durch eine exklusiv-oder-Verknüpfung über alle Datenbytes der
# Nutzinformation, inclusive der Endekennung DLE, ETX gebildet.
# Für die Informationszeichen ist kein Code vorgeschrieben (Codetransparenz).
#
# *****************************
# Senden mit der Prozedur 3964R
# Zum Aufbau der Verbindung sendet die Prozedur 3964R das Steuerzeichen STX aus. Antwortet das
# Peripheriegerät vor Ablauf der Quittungsverzugzeit (QVZ) von 2 sec mit dem Zeichen DLE, so geht die
# Prozedur in den Sendebetrieb über. Antwortet das Peripheriegerät mit NAK, einem beliebigen anderen
# Zeichen (außer DLE) oder die Quittungsverzugszeit verstreicht ohne Reaktion, so ist der
# Verbindungsaufbau gescheitert. Nach insgesamt drei vergeblichen Versuchen bricht die Prozedur das
# Verfahren ab und meldet dem Interpreter den Fehler im Verbindungsaufbau.
#
# Gelingt der Verbindungsaufbau, so werden nun die im aktuellen Ausgabepuffer enthaltenen
# Nutzinformationszeichen mit der gewählten Übertragungsgeschwindigkeit an das Peripheriegerät
# gesendet. Das Peripheriegerät soll die ankommenden Zeichen in Ihrem zeitlichen Abstand überwachen.
# Der Abstand zwischen zwei Zeichen darf nicht mehr als die Zeichenverzugszeit (ZVZ) von 220 ms
# betragen.
#
# Jedes im Puffer vorgefundene Zeichen DLE wird als zwei Zeichen DLE gesendet. Dabei wird das Zeichen
# DLE zweimal in die Prüfsumme übernommen.
#
# Nach erfolgtem senden des Pufferinhalts fügt die Prozedur die Zeichen DLE, ETX und BCC als
# Endekennung an und wartet auf ein Quittungszeichen. Sendet das Peripheriegerät innerhalb der
# Quittungsverzugszeit QVZ das Zeichen DLE, so wurde der Datenblock fehlerfrei übernommen. Antwortet
# das Peripheriegerät mit NAK, einem beliebigen anderen Zeichen (außer DLE), einem gestörten Zeichen
# oder die Quittungsverzugszeit verstreicht ohne Reaktion, so wiederholt die Prozedur das Senden des
# Datenblocks. Nach insgesamt sechs vergeblichen Versuchen, den Datenblock zu senden, bricht die
# Prozedur das Verfahren ab und meldet dem Interpreter den Fehler im Verbindungsaufbau.
#
# Sendet das Peripheriegerät während einer laufenden Sendung das Zeichen NAK, so beendet die
# Prozedur den Block und wiederholt in der oben beschriebenen Weise.
#
# Beispiel für einen fehlerlosen Datenverkehr:
# Prozedur 3964R Peripheriegerät
# STX           ->
#               <- DLE
# 1. Zeichen    ->
#               ->
#               ->
#               ->
# n. Zeichen    ->
# DLE           ->
# ETX           ->
# BCC           ->
#               <- DLE
#
# ********************************
# Empfangen mit der Prozedur 3964R
# Im Ruhezustand, wenn kein Sendeauftrag und kein Warteauftrag des Interpreters zu bearbeiten ist, wartet
# die Prozedur auf den Verbindungsaufbau durch das Peripheriegerät. Empfängt die Prozedur ein STX und
# steht ihr ein leerer Eingabepuffer zur Verfügung, wird mit DLE geantwortet.
#
# Nachfolgende Empfangszeichen werden nun in dem Eingabepuffer abgelegt. Werden zwei aufeinander
# folgende Zeichen DLE empfangen, wird nur ein DLE in den Eingabepuffer übernommen.
# Nach jedem Empfangszeichen wird während der Zeichenverzugszeit (ZVZ) auf das nächste Zeichen
# gewartet. Verstreicht die Zeichenverzugszeit ohne Empfang, wird das Zeichen NAK an das
# Peripheriegerät gesendet und der Fehler an den Interpreter gemeldet.
#
# Mit erkennen der Zeichenfolge DLE, ETX und BCC beendet die Prozedur den Empfang und sendet DLE
# für einen fehlerfrei (oder NAK für einen fehlerhaft) empfangenen Block an das Peripheriegerät.
# Treten während des Empfangs Übertragungsfehler auf (verlorenes Zeichen, Rahmenfehler), wird der
# Empfang bis zum Verbindungsabbau weitergeführt und NAK an das Peripheriegerät gesendet. Dann wird
# eine Wiederholung des Blocks erwartet. Kann der Block auch nach insgesamt sechs Versuchen nicht
# fehlerfrei empfangen werden, oder wird die Wiederholung vom Peripheriegerät nicht innerhalb der
# Blockwartezeit von 4 sec gestartet, bricht die Prozedur 3964R den Empfang ab und meldet den Fehler an
# den Interpreter.
#
# Beispiel für einen fehlerlosen Datenverkehr:
# Prozedur 3964R       Peripheriegerät
#                 <-      STX
#   DLE           -> 
#                 <-    1. Zeichen
#                 <-
#                 <-
#                 <-
#                 <-    n. Zeichen
#                 <-      DLE
#                 <-      ETX
#                 <-      BCC
#   DLE           ->
#
# ************************
# Initialisierungskonflikt
# Antwortet ein Gerät auf den Sendewunsch (Zeichen STX) seines Peripheriegerätes innerhalb der
# Quittungsverzugszeit QVZ nicht mit der Quittung DLE oder NAK, sondern ebenfalls mit dem Zeichen STX,
# liegt ein Initialisierungskonflikt vor. Beide Geräte möchten einen vorliegenden Sendeauftrag ausführen.
# Das Gerät mit der niederen Priorität stellt seinen Sendeauftrag zurück und antwortet mit dem Zeichen
# DLE. Das Gerät mit der höheren Priorität sendet daraufhin seine Daten in der vorher beschriebenen
# Weise. Nach dem Verbindungsabbau kann das Gerät mit der niederen Priorität seinen Sendeauftrag
# ausführen.
#
# niedrige Priorität     höhere Priorität
#   STX           ->
#                 <-     STX  (Konflikt)
#   DLE           -> 
#                 <-    1. Zeichen
#                 <-
#                 <-
#                 <-
#                 <-    n. Zeichen
#                 <-      DLE
#                 <-      ETX
#                 <-      BCC
#   DLE           ->
#
#
# Klassendifinition für den 3964r Treiber
# Der Treiber bedient eine Schnittstelle, welche definiert werden muss
class Dust3964r (stepchain,Serial):
    lock = threading.Lock()
    
    sendtry     = 0       # Anzahl der Sendeversuche
    sendbuff    = b""     # Sendepuffer ist leer
    readbuff    = b""     # Empfangspuffer
    telegrammOut= []      # Sendetelegramm (Puffer)
    MODE        = True    # Mit Blockprüfzeichen
    CFG_PRIO    = True    # Treiber läuft mit hoher Priorität
    CFG_PRINT   = True    # Modus Print eingeschalet (
    ETX_EN      = False   # Sequenzer erkennt, ob ein ETX nach einem DLE gültig ist
    BCC_EN      = False   # Sequenzer erkennt, das nach DLE, ETX nun das BCC folgen muss
    RealRun     = True    # Lauf im Simulator = false, in Realität True
    RUN         = False   # Der treiber läuft
    STX         = b"\x02" # chr(0x02)
    ETX         = b"\x03" # chr(0x03)
    DLE         = b"\x10" # chr(0x10)
    NAK         = b"\x15" # chr(0x15)
    LOPRIO      = False   # niedrige Prio
    HIPRIO      = True    # hohe Priorität
    M3964       = False   # Treiber läuft als 3964 ohne BCC Blocksumme
    M3964R      = True    # Treiber läuft als 3964r mit BCC Blocksumme

    def __init__ (self,port=None,baudrate=9600,QVZ=2.0,ZVZ=0.22,BWZ=4.0,CWZ = 3.0,SPZ=0.5,SLP= 1.4,MAXSEND=6,MAXCONNECT=6,PRIO=HIPRIO, MODE=M3964R):
        # Initialisierung der Schrittkettenklasse
        stepchain.__init__ (self)
        # Initialisierung der SchnrittstellenKlasse
        self.RS232     = Serial (port=port,baudrate=baudrate)
        self.QVZ       = QVZ        # Quittungsverzug ist 2.0 Sekunden (Buderus Doku)
        self.ZVZ       = ZVZ        # Zeichenverzugszeit 220ms (Buderus Doku)
        self.BWZ       = BWZ        # Blockwartezeit 4.0 Sekunden (Buderus Doku)
        self.CWZ       = CWZ        # Connectwartezeit 2.0 Sekunden (Wartezeit nach versuch fehlerhafter Verbindungsaufbau
        self.SPZ       = SPZ        # SendePause zeit (nach einem erfolgreichem Senden warten bis nächstes Senden
        self.SLP       = SLP        # Schlafenszeit vor dem Absenden vom DLE (muss klener als QVZ der Gegenseite sein)
        self.MAXSEND   = MAXSEND    # Maximalanzahl Sendeversuche, danach wird das Telegramm verworfen
        self.MAXCONNECT= MAXCONNECT # Anzahl maximaler Verbindungsaufbau Versuche
        self.sendERR   = 0          # Sendefehler auf 0
        self.connectERR= 0          # Verbindungsaufbau Fehler auf 0
        self.RUN       = False      # Treiber in Stop
        self.SendAtTime= 0          # Erlaube Senden ab dem Zeitpunkt
        self.MODE      = MODE       # Treibermodus einstellen (Serienmäßig nach dem Start: 3964r mit Blocksumme
        self.CFG_PRIO  = PRIO       # Modus einstellen
        self.telegrammOut= []       # Ausgangspuffer ist leer
        self.RS232.flushOutput ()   # puffer tillen
        self.RS232.flushInput ()
        self.RS232.write (self.NAK) # auf der schnittstelle mal blind am anfang ein NAK raushauen
        

    # hiermit kann der Modus des Treibers umgeschaltet werden. der Aufruf kann nur nach dem INIT gemacht werden, nicht während des Laufens
    # Mögliche Mode sind: PRIO = LO   : Bei einem INIT Konflikt stellt Treiber seinen Sendewunsch zurück
    #                     PRIO = HI   : Bei einem Init Konflikt besteht der Treiber auf Bestätigung Sendebereitschaft durch die Gegenseite
    #                     MODUS= 3964 : Übertragung ohne Blockprüfkennung BCC
    #                     MODUS= 3964r: Übertragung mit Blockprüfkennung BCC
    def mode (self,PRIO,MODE):
        if not self.RUN:
            self.CFG_PRIO= PRIO
            self.MODE    = MODE

    # Berechnet das XOR CRC für den übergebenen buff
    # buffer sollte ein bytestring sein
    # der Rückgabewert ist ein bytestring
    def crc (self,buffer):
        bcc= 0x00
        for c in buffer:
            bcc ^= c
        return bytes([bcc])

    # wandelt den buffer in den auszugebenen bytestring um
    # erwartet buffer als bytestrng
    # Rückgabewert ist ebenfalls ein bytestring
    def outframe (self,buffer):
        # Ein DLE in Datenstring führt zur Verdopplung von DLE
        # DLE und ETX sind der Frame vom 3964r Protokoll
        puffer= buffer.replace (self.DLE,self.DLE+self.DLE)+ self.DLE + self.ETX
        # prüfen, welches Protokoll: 3964= ohne BCC, 3964r mit BCC
        if self.MODE:
            puffer+=self.crc (puffer)
        return puffer

    # Befreit das Telegramm von dem 3964r Frame, prüft die Checksumme
    # Der buffer muss ein bytestring sein
    # Rückgabe: NONE, wenn irgendein Fehler aufgetreten ist, Telegramm verstümmelt, Checksum falsch
    # Wenn alles OK, Rückgabe des Telegramms als Bytestring
    def inframe (self,buffer):
        stream= buffer
        # BCC Kontrolle nur im 3964r modus
        if self.MODE:
            try:
                bufferBCC= stream[-1:]
                stream= buffer[:-1]
            except: return None
            # testen ob der Checksumme zu dem Stream passt
            if bufferBCC!=self.crc (stream):
                return None
        try:
            # Nun prüfen, ob am Ende vom Stream DLW und ETX drinsind
            if stream[-2:]!=self.DLE+self.ETX:
                return None
        except: # Inframe buffer zu klein
            return None
        return stream [:-2].replace (self.DLE+self.DLE,self.DLE)    
    
    def sendstream (self,sendepuffer):       
        buffer= self.outframe (sendepuffer)
        self.RS232.write (buffer)
        if self.CFG_PRINT:
            print (" 10r",end="")
            for c in buffer:
                print("%3.2X"% c + "s",end="")
        # der puffer wurde über die rs232 ausgegeben

    # Fehler in der Kommunikation: NAK ausgeben
    # Bei einem NAK wird immer auch ein flush ausgeführt
    def errNAK (self):
        self.RS232.flushOutput ()
        self.RS232.flushInput ()
        self.RS232.write (self.NAK+self.NAK+self.NAK)
        self.setnewstep (0)

    # eine Verzögerungszeit für das nächste Senden wird definiert
    def SetSendDelay (self,sec):
        self.SendAtTime= t.time ()+sec

    # Read Success wird aufgerufen, wenn ein Telegramm erfolgreich eingelesen wurde
    # Virtuelle Routine, muss überladen werden vom child    
    def ReadSuccess (self,telegram):
        pass

    # WriteFail wird aufgerufen, wenn win Telegramm verworfen wurde nach 6 Sendeversuchen
    # Virtuelle Routine, muss überladen werden vom child    
    def WriteFail (self,telegram):     
        pass

    # Write Success wird aufgerufen, wenn ein Telegram erfolgreich versendet worden ist
    # Virtuelle Routine, muss überladen werden vom child
    def WriteSuccess (self,telegram):
        pass

    # Routine Prüft, ob im Sendepuffer ein Auftrag vorhanden ist
    def isJob (self):
        return self.telegrammOut!=[]

    # Routine fügt einen neunen Sendeauftrag in den Puffer ein
    def newJob (self,job):
        Dust3964r.lock.acquire() # Thread blockieren, der part nun hier muss atomar sein
        self.telegrammOut.append (job)
        Dust3964r.lock.release() #
        if self.CFG_PRINT:
            print ("NEUER JOB EINGEGANGEN: ",job)

    # Routine nimmt den ältesten Sendeauftrag aus der Liste und gibt diesen Zurück
    # existiert kein Job, wird NONE zurückgegeben
    def getJob (self):    
        if not self.isJob ():
            return None   # Kein Job in der Liste
        Dust3964r.lock.acquire() # Thread blockieren, der part nun hier muss atomar sein
        job= self.telegrammOut [0]
        self.telegrammOut= self.telegrammOut [1:]
        Dust3964r.lock.release() #
        if self.CFG_PRINT:
            print ("JOB WIRD BEARBEITET: ", job)
        return job
        

    # Schritt 0: Der Grundschritt:
    # Steht kein aktuelles Kommando zur Ausführung an und ist inWaiting() <>0 (Zeichen im Buffer)
    # Dann neuer Schritt = 1 (Empfang überprüfen)
    def schritt_0 (self):
        if self.newstep: # Einmaliger Durchlauf in dem Schritt
            # Initialisierung der Werte
            if (self.sendERR==self.MAXSEND) or (self.connectERR==self.MAXCONNECT):
                self.WriteFail (self.sendbuff)
                self.sendbuff=b""        #
                if self.CFG_PRINT:
                    print(t.strftime("%H:%M:%S")+"."+ "%6.6d"% datetime.now().microsecond + ": Telegramm verworfen nach " , self.MAXSEND , " Fehlversuchen")
                self.sendERR=0
                self.connectERR=0
        if (self.sendbuff==b"") and self.isJob ():
            job= self.getJob ()
            if not (job is None):
                self.sendbuff=job
        self.SEND_EN= (len(self.sendbuff)!=0) and (t.time ()>self.SendAtTime)        
        if self.RS232.inWaiting () and self.RealRun:
            # Es ist ein Zeichen im Empfangspuffer
            # An dieser Stelle kann und darf es höchstens das Zeichen STX sein
            char= self.RS232.read ()
            if char != self.STX:
                # Es war kein STX, das ist auf jedenfall mal ein Fehler also: NAK senden
                if self.CFG_PRINT:
                    print(t.strftime("%H:%M:%S")+"."+ "%6.6d"% datetime.now().microsecond + ":[RX]"+ "%3.2X"% ord (char) + "r 15s [NAK: STX-START]")                  
                self.errNAK ()
            else: # Es war ein STX
                if not self.CFG_PRIO or not self.SEND_EN: # Treiber hat niedrige PRIO oder nix zum senden
                    if self.CFG_PRINT:
                        print(t.strftime("%H:%M:%S")+"."+ "%6.6d"% datetime.now().microsecond + ":[RX] 02r",end="")
                    t.sleep (self.SLP) # Für die erlaubte Antwortzeit legt sich der Prozess schlafen    
                    self.setnewstep (4) # Verbindungsaufbau 3964r läuft nun ready to receive
                elif self.SEND_EN:
                    if self.CFG_PRINT:
                        print(t.strftime("%H:%M:%S")+"."+ "%6.6d"% datetime.now().microsecond + ":[TX] 02r 02s",end="")
                    self.RS232.flushOutput ()
                    self.RS232.write (self.STX)
                    self.setnewstep (1) # verbindungsaufbau mit Konflikt: wir wollen Senden mit Hiprio
        else: # Es gibt kein Zeichen im Empfangspuffer
            if self.SEND_EN: # wir haben was zu senden
                if self.CFG_PRINT:
                    print(t.strftime("%H:%M:%S")+"."+ "%6.6d"% datetime.now().microsecond + ":[TX] 02s",end="")
                self.RS232.flushInput ()    
                self.RS232.flushOutput ()
                self.RS232.write (self.STX)
                self.setnewstep (3) # Verbindungsaufbau von uns kommt

    # Schritt 1: Senden (wir haben STX gesenden und erwarten ein DLE
    # Es muss ein DLE innerhalb der quittungsverzugszeit kommen
    # Alles was nicht DLE ist grund für ein NAK (Hi prio)
    def schritt_1 (self):
        if self.stepdauer>self.QVZ:
            # Quittungsverzugszeit ist abgelaufen
            if self.CFG_PRINT:
                print (" 15s [NAK: QVZ-START]")
            self.connectERR +=1 # Verbindungsaufbaufehler um 1 erhöhen
            self.SetSendDelay (self.CWZ)
            self.errNAK ()
        elif self.RS232.inWaiting ():
            # Zeichen wurde eingelesen, es muss ein DLE sein
            c= self.RS232.read ()
            if c!= self.DLE:
                # Es war aber kein DLE
                if self.CFG_PRINT:
                    print ("%3.2X"% ord (c) + "r 15s [NAK: DLE-START]")
                self.connectERR +=1 # Verbindungsaufbaufehler um 1 erhöhen
                self.SetSendDelay (self.CWZ)
                self.errNAK ()
            else: # es war ein DLE, senden ausführen
                self.sendstream (self.sendbuff)
                self.setnewstep (2)

    # Schritt 2: Gesendeter Datenstream muss mit DLE vom Empfänger bestätigt werden
    # DLE muss innerhalt der QVZ kommen
    def schritt_2 (self):
        if self.stepdauer>self.QVZ:
            # Quittungsverzugszeit ist abgelaufen
            if self.CFG_PRINT:
                print (" 15s [NAK: QVZ-BCC]")
            self.sendERR +=1 # sendefehler um 1 erhöhen
            self.SetSendDelay (self.BWZ)
            self.errNAK ()
        elif self.RS232.inWaiting ():
            # Zeichen wurde eingelesen, es muss ein DLE sein
            c= self.RS232.read ()
            if c!= self.DLE:            
                # Es war aber kein DLE
                if self.CFG_PRINT:
                    print ("%3.2X"% ord (c) + "r 15s [NAK: DLE-BCC]")
                self.sendERR +=1 # Verbindungsaufbaufehler um 1 erhöhen
                self.SetSendDelay (self.BWZ)
                self.errNAK ()
            else: # es war ein DLE, Telegramm wurde erfolgreich versendet
                if self.CFG_PRINT:
                    print (" 10r [OK]")
                self.WriteSuccess (self.sendbuff) # Virtuelle Routine
                self.sendbuff=b"" # Sendepuffer löschen, das telegramm austragen
                self.SetSendDelay (self.SPZ)
                self.setnewstep (0)

    # Schritt 3: Verbindungsaufbau von uns angestossen, wir wollen senden
    # Von uns wurde ein STX gesendet, es darf nun als Antwort kommen:
    # STX: Der Partner will selber senden
    # DLE: Alles ok, wir senden
    def schritt_3 (self):
        if self.stepdauer>self.QVZ:
            # Quittungsverzugszeit ist abgelaufen
            if self.CFG_PRINT:
                print (" 15s [NAK: QVZ-DLE START]")
            self.sendERR +=1 # sendefehler um 1 erhöhen
            self.SetSendDelay (self.CWZ)
            self.errNAK ()
        elif self.RS232.inWaiting ():
            c= self.RS232.read ()
            if c== self.DLE:
                # Das eingelesene Zeichen ist ein DLE
                # wunderbar, alles, ok, wir können senden
                self.sendstream (self.sendbuff)
                # Nach dem Senden muss mit DLE vom empfänger bestätigt werden
                self.setnewstep (2)
            elif c== self.STX:
                # Wir bekommen als Antwort auf unser STX ebenfalls ein STX zurück
                # der Klassische Initialisierungskonflikt
                if not self.CFG_PRIO:
                    # Unser Treiber hat Low Prio also alles OK, wir müssen mit DLE antworten
                    # Es folgt nun ganz normales Empfangen
                    if self.CFG_PRINT:
                        print (" 02r",end="")
                    self.setnewstep (4)
                else: # Nu gibts ein Problem.
                    # Unserer Treiber läuft auf High Prio, und die Gegenseite setzte auch ein STX ab
                    # Die Gegenseite ist also auch im High Prio Mode
                    # das ist eine etwas unkluge Sache in dem Moment
                    # wir geben ein NAK raus und initialieren neu
                    # REV 2. wir geben trotz High Prio nach und senden ein DLE
                    #if self.CFG_PRINT:
                    #    print (" 02r",end="")
                    #self.setnewstep (4)   
                    
                    if self.CFG_PRINT:
                        print (" 02r 15s [NAK: STX-STX PRIO]")
                    self.connectERR +=1 # connectfehler um 1 erhöhen
                    self.SetSendDelay (0)
                    self.errNAK ()
            else:
                print ("%3.2X"% ord (c) + "r 15s [NAK: DLE-START]")
                self.connectERR +=1 # Verbindungsaufbaufehler um 1 erhöhen             
                self.SetSendDelay (self.CWZ)                
                self.errNAK ()
      

    # Schritt 4: Empfangen der Daten, Verbindungsaufbau
    # Es wird das DLE gesendet f+r_ wir sind empfangsbereit
    # Danach muss innerhalb der QVZ der Stream beginnen
    def schritt_4 (self):
        if self.newstep:
            if self.CFG_PRINT:
                print (" 10s",end="")
            self.RS232.flushOutput ()
            self.RS232.flushInput ()
            self.RS232.write (self.DLE)          
        # Nach dem DLE muss nun innerhalt der ZVZ der Datenstream beginnen
        if self.stepdauer>self.ZVZ:
            # Zeichenverzugszeit ist abgelaufen NAK fehler
            if self.CFG_PRINT:
                print (" 15s [NAK: ZVZ-START]")            
            self.errNAK ()  
        elif self.RS232.inWaiting ():
            # Zeichen innerhalb der Zeit im Puffer, alles ist gut
            self.setnewstep (5)  

    # Schritt 5: Empfangen Datenstream
    # der Datenstream wird empfangen, bis der Parser die Sequenz DLE ETX erkennt
    def schritt_5 (self):
        # Wenn der Schritt neu aufgerufen wird, dann STX_EN auf False setzen
        if self.newstep:
            self.STX_EN  =False
            self.BCC_EN  =False
            self.readbuff=b""
        # Abfrage der Zeichenverzugszeit
        # Zeichenverzug ist aufgetreten (NAK wird gesendet)
        # Empfangsfehler hochzählen
        if t.time ()-self.starttime > self.ZVZ:
            if self.CFG_PRINT:
                print (" 15s [NAK: ERR-ZVZ]")
            self.errNAK ()
        else:    
            #solange wie zeichen im puffer oder EndeStream nicht erkannt    
            while self.RS232.inWaiting():
                #zeichen aus dem puffer lesen
                c=self.RS232.read ()
                self.starttime=t.time () # Zeit setzen beim letzten Empfangenen Zeichen
                if self.CFG_PRINT:
                    print("%3.2X"% ord (c) + "r",end="")
                self.readbuff=self.readbuff+c    
                if self.BCC_EN:
                    rec=self.inframe (self.readbuff)
                    if rec is None:
                        # Fehler beim Zerlegen vom Inframe oder Checksum fehler
                        if self.CFG_PRINT:
                            print (" 15s [NAK: ERR-BCC]")
                        self.errNAK ()
                    else:    
                        if self.CFG_PRINT:
                            print (" 10s [DLE: OK]")
                        self.ReadSuccess (rec)                               
                        self.RS232.flushInput ()
                        self.RS232.flushOutput ()                      
                        t.sleep (self.SLP) # Für die erlaubte Quittungsverzugszeit legt sich der Prozess mal schlafen
                        self.RS232.write (self.DLE)
                    self.setnewstep (0)    
                    break
                elif (c==self.DLE):
                    self.STX_EN= not (self.STX_EN)
                elif ((c==self.ETX) and self.STX_EN):
                    self.BCC_EN= True # Endekennung gültig erkannt, nun das BCC als letztes Zeichen
                else:
                    self.STX_EN=False
                    self.BCC_EN=False
        return

    
    # Wird immer ausgeführt vor den Schritten
    def schritt (self):
        options = {0 : self.schritt_0,1: self.schritt_1, 2: self.schritt_2, 3: self.schritt_3, 4: self.schritt_4, 5: self.schritt_5}
        options [self.step]()

schritte.py:


#!usr/bin/python3
# -*-coding:Utf-8 -*

#https://homematic-forum.de/forum/viewtopic.php?t=26955&start=20

import time as t

# Grundklasse einer Schrittkette
class stepchain:
    step=0
    laststep=0
    nextstep=0
    newstep=True
    starttime=t.time ()
    stepdauer=0

    def __init__ (self,):
        self.step=255  # 255 = Initialisierung, bei Neustart mit Run erfolgt auf jedenfall ein newstep=true
        self.laststep=0
        self.nextstep=0
        self.newstep=True
        self.starttime=t.time ()
        self.stepdauer=0

    def schritt (self):
        pass

    def running (self):
        self.newstep=self.step!=self.nextstep # Schrittwechsel erkannt
        if self.newstep:
            self.starttime=t.time ()
            self.laststep=self.step
        self.stepdauer=t.time ()-self.starttime
        self.step=self.nextstep
        # Aufruf: wird immer vor den Schritten ausgeführt
        self.schritt()

    def setnewstep (self,step):
        self.nextstep= step

    def schrittDauer (self):
        return t.time ()-self.starttime

    def triggerDauer (self):
        self.starttime=t.time ()