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.
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")