Script for reading a heat meter Itron Cf Echo 2

I have a heat meter at home and can read it and display the data in Home Assistant.

Things required:

  1. USB reading head (e.g. Hichi USB reading head)

  2. Python Script Pro Integration

  3. Now the USB reading head is connected to Home Assistant and the Python Script Pro Integration is installed. Now the following script is uploaded to Home Assistant, e.g. with the File Editor Addon.

  4. The template sensors must then be created

  5. The script can now be called with an automation and the data should be transferred successfully

How the script works:

  • A connection is established to the USB reader

  • Data is converted,

  • Transfer of data to Home Assistant

Sensoren anlegen:

# Wärmmemengenzähler sensoren
mqtt:
  sensor:
    - name: "Energieverbrauch"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.ENERGY_WH" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: "Wh"
      unique_id: wmz_energieverbrauch
      device_class: energy
      state_class: total_increasing
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"
      
    - name: "Volumenstrom"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.VOLUME" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: "m3"
      unique_id: wmz_volumen
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"
      
    - name: "Power"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.POWER_W" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: "W"
      unique_id: wmz_Power
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"
    
    - name: "Vorlauf"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.FLOW_TEMPERATURE" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: C
      unique_id: wmz_vorlauf
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"
      
    - name: "Rücklauf"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.RETURN_TEMPERATURE" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: "C"
      unique_id: wmz_rücklauf
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"

    - name: "Differenz"
      state_topic: "mbus/data"
      value_template: >-
        {% for record in value_json.body.records %}
          {% if record.type == "VIFUnit.TEMPERATURE_DIFFERENCE" %}
            {{ record.value }}
          {% endif %}
        {% endfor %}
      unit_of_measurement: "K" 
      unique_id: wmz_differenz
      device:
        name: "Wärmemengenzähler"
        identifiers:
          - "22835426"
        manufacturer: "Itron"
        model: "CF Echo 2"

Script zum auslesen der Daten:

import serial
import time
import binascii
import logging
import meterbus
import paho.mqtt.client as mqtt
import json

# Logger konfigurieren
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# MQTT Konfiguration (Anpassen!)
MQTT_BROKER = "xxx"  # z.B. 192.168.178.20
MQTT_PORT = 1883
MQTT_USER = "xxx"
MQTT_PASSWORD = "xxx"
MQTT_TOPIC = "mbus/data"

# MQTT Callbacks
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        logger.info("Erfolgreich mit MQTT Broker verbunden!")
    else:
        logger.error(f"Verbindung mit MQTT Broker fehlgeschlagen, Fehlercode: {rc}")

def on_publish(client, userdata, mid):
    logger.info(f"Daten erfolgreich an MQTT gesendet (MID: {mid})")

def on_disconnect(client, userdata, rc):
    if rc != 0:
        logger.warning(f"Verbindung zum MQTT Broker getrennt, Fehlercode: {rc}. Versuche erneut zu verbinden...")
        client.reconnect()  # Automatische Wiederverbindung
    else:
        logger.info("Erfolgreich vom MQTT Broker getrennt.")

# === get_data ==================================================================================
def get_data(ser=None, test_data=None):
    if test_data:
        logger.info("Verwende Testdaten...")
        return test_data
    else:
        try:
            ser.write(b'\x55' * 528)
            time.sleep(0.350)
            ser.parity = serial.PARITY_EVEN

            logger.info("Daten lesen")
            read_data = b'\x10\x5B\xFE\x59\x16'
            ser.write(read_data)

            result = ser.read(620)
            if not result:
                logger.warning("Keine Daten vom seriellen Port empfangen.")
                return None

            byte_array_hex = binascii.hexlify(result)
            logger.info(f"gelesene Daten (hex): {byte_array_hex.decode()}")
            return result

        except serial.SerialException as e:
            logger.error(f"Fehler bei der seriellen Kommunikation in get_data: {e}")
            return None

# === daten_filtern ==============================================================================
def daten_filtern(daten):
    if daten is None:
        return None

    start_byte = b'\x68'
    end_byte = b'\x16'

    start_index = 0
    while True:
        try:
            start_index = daten.index(start_byte, start_index)
            end_index = daten.find(end_byte, start_index + 1)
            if end_index != -1:
                gefilterte_daten = daten[start_index: end_index + 1]
                logger.info(f"Gefilterte Daten (hex): {binascii.hexlify(gefilterte_daten).decode()}")
                return gefilterte_daten
            else:
                logger.warning("Kein Endbyte nach dem Startbyte gefunden.")
                return None
        except ValueError:
            logger.warning("Kein Startbyte mehr gefunden.")
            return None
        start_index += 1

def transform_json(json_string):
    """
    Transformiert einen JSON-String.
    """
    try:
        data = json.loads(json_string)
    except json.JSONDecodeError:
        # Fließkommazahlen runden
        for record in body_content.get("records", []):
            if isinstance(record.get("value"), float):
                record["value"] = round(record["value"], 2)

        data.update(body_content)
    return json.dumps(data, indent=4)

# === main ========================================================================================
logger.info('Starte Hauptprogramm')
try:
    # Testdaten definieren
    #test_daten_hex = "684d4d680800722654832277040904360000000c78265483220406493500000c14490508000b2d0000000b3b0000000a5a18060a5e89020b61883200046d0d0c2c310227c80309fd0e2209fd0f470f00008d16"
    #test_daten = bytes.fromhex(test_daten_hex)

    # Entweder mit Testdaten testen...
    #roh_daten = get_data(test_data=test_daten)

    #...oder mit der seriellen Schnittstelle (auskommentieren für Tests!)
    ser = serial.Serial("/dev/ttyUSB1", baudrate=2400, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=10)
    roh_daten = get_data(ser)
    ser.close()

    if roh_daten:
        gefilterte_daten = daten_filtern(roh_daten)

        if gefilterte_daten:
            try:
                frame = meterbus.load(gefilterte_daten)
                json_data = frame.to_JSON()
                logger.info(f"M-Bus Daten (JSON): {json_data}")

                # JSON transformieren
                transformed_json = transform_json(json_data)
                if transformed_json:
                    logger.info(f"Transformierte M-Bus Daten (JSON): {transformed_json}")
                    json_data_to_mqtt = transformed_json # Verwende die transformierte JSON für MQTT
                else:
                    logger.error("Fehler bei der JSON Transformation. Sende untransformierte Daten.")
                    json_data_to_mqtt = json_data # Bei Fehler die ursprünglichen Daten senden

                # MQTT Client erstellen und konfigurieren
                client = mqtt.Client()
                if MQTT_USER and MQTT_PASSWORD:
                    client.username_pw_set(MQTT_USER, MQTT_PASSWORD)

                # Callbacks setzen
                client.on_connect = on_connect
                client.on_publish = on_publish
                client.on_disconnect = on_disconnect

                try:
                    client.connect(MQTT_BROKER, MQTT_PORT, 60)
                    client.loop_start()
                    client.publish(MQTT_TOPIC, json_data_to_mqtt) # Sende die (transformierten) JSON Daten
                    time.sleep(1)
                    client.loop_stop()
                    client.disconnect()
                    logger.info("MQTT Operationen abgeschlossen.")

                except Exception as e:
                    logger.error(f"Fehler bei der MQTT Verbindung oder beim Senden: {e}")

            except meterbus.exceptions.MBusFrameDecodeError as e:
                logger.error(f"Fehler beim Decodieren des M-Bus Frames: {e}")
            except Exception as e:
                logger.error(f"Unerwarteter Fehler beim Parsen des M-Bus Frames: {e}")
        else:
            logger.info("Keine gültigen Daten zum Filtern gefunden.")
    else:
        logger.info("Keine Daten empfangen (weder seriell noch Testdaten).")

except serial.SerialException as e:
    logger.error(f"Fehler bei der seriellen Kommunikation im Hauptprogramm: {e}")
except Exception as e:
    logger.error(f"Unerwarteter Fehler im Hauptprogramm: {e}")

logger.info('Programm beendet')

Aufrufen des Scripts:


action: python_script.exec
data:
  file: wmz_mbus/wmz_mbus.py

1 Like

How often do you read the data?
In the manual is written that it shouldn’t be read more than once an hour because of the battery.

Thanks @thorstenniemann.tn, I adapted and turned your logic into a script for the bitShake SmartMeterReader Air which can be found here in the section for Itron CF Echo II.

I will also post it here for future reference in case that website goes down:

>D
done=0
wkup=1
>B
smlj=0
->sensor53 r
>BS
=#readmeter
>S
if sb(tstamp 14 2)=="30"
and done==0
then
=#readmeter
done=1
print done set
endif
if sb(tstamp 14 2)=="31"
and done==1
then
done=0
print done reset
endif
if (sml[2]>0
and sml[10]>0)
and smlj==0
then
smlj=1
print enabled MQTT
endif
if (sml[2]==0
or sml[10]==0)
and smlj==1
then
smlj=0
print disabled MQTT
endif
#readmeter
print wakeup start
;set serial protocol
sml(-1 1 "2400:8N1")
;send 0x55 for 2,2 seconds with 8N1 (53x), 2400 baud (wakeup sequence)
for wkup 1 53 1
sml(1 1 "55555555555555555555")
next
print wakeup end
wkup=1
print wait for the meter
delay(350)
;switch serial protocol
sml(-1 1 "2400:8E1")
print request data
;request data with "105B005B16"
sml(1 1 "105BFE5916")
>M 1
+1,5,rE1,0,2400,WAERME,4
1,=so3,16
1,=soC,1024,3
1,0C78bcd8@1,Fabrication number,no,fabrication_no,0
1,0C78xxxxxxxx0406uuUUuuUUs@1000,Total energy,MWh,total_energy,3
1,0406xxxxxxxx0C14bcd8@100,Total volume,m³,total_volume,2
1,0C14xxxxxxxx0B2Dbcd6@10,Current power,kW,current_power,2
1,0B2Dxxxxxx0B3Bbcd6@1000,Current volume flow,m³/h,current_volume_flow,3
1,0B3Bxxxxxx0A5Abcd4@10,Flow temperature,°C,temp_flow,1
1,0A5Axxxx0A5Ebcd4@10,Return temperature,°C,temp_return,1
1,0A5Exxxx0B61bcd6@100,Temperature difference,°C,temp_diff,2
1,0B61xxxxxx046DuuUUuuUUs@1,Date and time,t,meter_time,0
1,046Dxxxxxxxx0227uuUU@1,Operating time days,d,meter_days,0
1,0227xxxx09FD0Ebcd2@1,Firmware version,v,firmware_version,0
1,0227xxxx09FD0Exx09FD0Fbcd2@1,Software version,v,software_version,0
#