Connecting to an Ultrasonic Heat Meter

Hi David,

I have the same model; opening it has the telltale M-Bus transceiver chip from TI TSS721A - but I also have not yet been able to communicate with the meter at all…

Did you ultimately find a solution?

Have you tried the IR interface as an alternative - maybe the wired M-Bus needs to be activated somehow before becoming responsive?

[Moved to new topic as previous topic was marked solved - Mod]

Hello, no never able to communicate with it, I gave up in the end after wasting a lot time, with no real explanation for the seller. At the beginning the display showed the correct numbers. It’s a shame it doesn’t work, after removing from my heating system I found lots rust particles in its pipe work and wondered even if had worked if it would have been accurate.

Did you try communicating with the IR sequence?

The ir led and receiver seem awfully close to the tss721 mbus transceiver

Or can you describe your attempts so i don’t have to repeat them all?

Do you still have the documentation which were mentioned in another forum? Since the heat meater has an internal date/time clock, there must be a way to set that up… Presumably the M-Bus could double as S0 interface (one pulse per x.xxx kWh), be disabled, or run the full M-Bus protocol… (I don’t know if the TSS721 could be made compatible with RS485 though.

Hello, I have many files in connection with the meter, I have made zip file here

energy meter.zip (8.5 MB)

take a look you might find something to get it working

Success. I received a document from the OEM in china via a colleague.

An initial test with 2400 baud, 7E1 eluded a response.
The multi-byte fields are least-significant byte first.

This type of meter requires a preamble to sync with the speed.

The document has 7 examples, and I simply implemented the first one:

FE FE FE FE FE FE 68 (20) (AA AA AA AA AA 11 11) (01) (03) (3F
90 12) (E1) 16

first 6 bytes of 0xFE - synchronization preamble

68 - frame start
20 - command

(1st bracket) - broadcast address, meter type 11 11

0x01 - control code - read  (according to a 188 standard protocol)
0x03 - data field length
0x903F - high-precision meter reading data identifier
0x12 - sequence number
0xe1 - checksum (arithmetic sum of all data from the byte after the frame start byte 0x68 until before the checksum byte),
0x16 - frame end

The response I got:

fe fe fe fe fe 68
(25) (25 32 26 24 00 00 00) (81) (3a) (3f 90 03) (72 24) (00
00 00 00) (00 00 00 00 2c) (00 00 00 00 05) (00 00 00)
(03 00) (00 00 00 00) (00 00 00) (00 00 00 00) (00 10) (00
10) (00 10) (00 10) (00 00 00 00 00 00) (44 24) (00) (29 06
24 20) (00 08) (a8) 16
0xfe - synchronization preamble
0x68 - frame start
0x25 - frame length (? - excluding checksum, including length field)
0x25 0x32 0x26 0x24 0x00 0x00 0x00 - the unit serial number in BCD, from the display (24263225)
0x81 - response (?9
0x3a -
0x3f 0x90 0x03  - request frame?
0x72 0x24 - 2472  - inlet temperature 24.72 Celsius
0x00 0x00 0x00 0x00 - number of pulses (?)
00 00 00 00 2c - cumulative flow (?), four bytes bcd the value, last hex byte the unit
00 00 00 00 05 - cumulative energy, four byted BCD for value, last hex for unit
00 00 00 - cumulative alarm time in BCD, hours
03 00 - nominal diameter:
0x0001 DN15
0x0002 DN20
0x0003 DN25
0x0004 DN32
0x0005 DN40
0x0006 DN50
0x0007 DN65
0x0008 DN80
0x0009 DN100
0x000A DN125
0x000B DN150
0x000C DN200
0x000D DN250
0x000E DN300
00 00 00 00 - Zero point offset
00 00 00 - Pulse width
00 00 00 00 - undefined
00 10 - 2.5 flow point, low byte first HEX. the actual value is divided by 4096. 0x1000 / 4096 = 1.
00 10 - 0.75 flow point, low byte first HEX, divided by 4096
00 10 - 0.25 flow point
00 10 - 0.05 flow point
00 00 00 00 00 00 - undefined
44 24 - outlet temperature BDC (24.44 Celsius)
00 - undefined
29 06 24 20 - date code BCD (2024-06-29)
00 08 - meter status (no water here).
a8 - checksum
16 - frame end

[Formatted by Mod] - please use Ctl-E to preformat code etc.

Seems like I got one of the same documents you got, but you actually collected more.

I am communication with 2400/7E1 (not 9600/8N1 as in the document).
And the sync preamble appears to have been a vital clue.

Also, it appears that one is supposed to calibrate the units (at flow rates of 0.05, 0.25 0.75 and 2.5 m³/h presumably) if accuarate results are necessary.

Anyway, I will try to communicated with two units on the same bus later this week, set the clock on all 7 units I have, and maybe tinker with the accuracy if i can be bothered.

Right now I’m happy the heat meater is talking back to me!

you are getting somewhere, are testing in a live circuit with fluids?

Not yet. But I could set the date/time - validate this in the A1 and A2 submenus on the meter. Here is a crude python script to do that - to convert the timestamp into BCD and calculate the checksum, append the preamble and trailer.

I wonder if i could calibrate them - in reality I am only really interested in the relative measurements among them as a group; if they all have similar systematic errors, and show e.g. 10% too much, but my utility meter (gas meter) which is there for actual billing - I can still divide the heat consumed by the different loops relative to each other and split the bill accordingly.

#!/usr/bin/python

import argparse
import serial
import time
import logging
logger = logging.getLogger(__name__)

def recv_frame(ser, length=1):
  data = b""
  frame = None

  while frame is None:
    characters = ser.read(length)

    if isinstance(characters, str):
      characters = bytearray(characters)

    if len(characters) == 0:
      break

    data += characters

#    logger.info('RECV ({0:03d}) {1}',format(len(characters), " ".join(["{:02x}".format(x).upper() for x in characters])))
#    print('===========  RECV ({0:03d}) {1}',format(len(characters), " ".join(["{:02x}".format(x).upper() for x in characters])), flush=True)
  print(" ".join(["{:02x}".format(x) for x in data]))

  if len(data):
    return False

  return None

def serial_send(ser, data):
  frame_data = bytearray(data)
  logger.info('SEND ({0:03d}) {1}'.format(len(data), " ".join(["{:02x}".format(x).upper() for x in frame_data])))
  print('SEND ({0:03d}) {1}'.format(len(data), " ".join(["{:02x}".format(x).upper() for x in frame_data])), flush=True)
  ser.write(bytearray(data))
  time.sleep(0.1)
  recv_frame(ser)



def checksum(frame):
    cs = 0;
    if (bytearray(frame)[0] != 0x68):
      print("Unexpected Frame Start Byte: {:02x}".format(bytearray(frame)[0]))
      return None
    for x in bytearray(frame):
      cs += int(x)
    print("checksum: :{:04x} - lowest bytes: {:02x}".format(cs, cs & 0xff))
    cs = cs & 0xff
    return bytearray(frame) + cs.to_bytes(1, 'big')

def build_frame(ser, data):
  print("buildframe")

  data = checksum(bytearray(data))
  data = b'\xFE\xFE\xFE\xFE\xFE\xFE' + data + b'\x16'
  serial_send(ser, data)

def bcd(value):
  if (int(value) >= 100):
    print("bcd conversion failed, value too large")
    return None
  print(value)
  bcd = 0x10 * int(value / 10) + (value % 10)
  return bcd.to_bytes(1, 'big')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Scan serial M-Bus for devices.')
    parser.add_argument('-d', action='store_true',
                        help='Enable verbose debug')
    parser.add_argument('-b', '--baudrate',
                        type=int, default=2400,
                        help='Serial bus baudrate')
    parser.add_argument('-r', '--retries',
                        type=int, default=5,
                        help='Number of ping retries for each address')
    parser.add_argument('device', type=str, help='Serial device or URI')

    args = parser.parse_args()

    logger.setLevel(logging.INFO)

# Generic all hi-res data
#    frame_data = b'\x68\x20\xAA\xAA\xAA\xAA\xAA\x11\x11\x01\x03\x3F\x90\x12'

#address from the meter - the command otherwise updates the number too.
    address = b'\x25\x32\x26\x24\x00\x11\x11'
    now = time.localtime()
    print("{}".format(bcd(int(now.tm_year/100))))

    frame_data = b'\x68\x20' + address + b'\x39\x11\x18\xa0\xaa' + address +\
      bcd(now.tm_sec) + bcd(now.tm_min) + bcd(now.tm_hour) + bcd(now.tm_mday) + bcd(now.tm_mon) + bcd(now.tm_year % 100) + bcd(int(now.tm_year/100))
#b'\x00\x31\x11\x27\x04\x26\x20'


    checksum(frame_data)

    try:
        with serial.serial_for_url(args.device,
                           args.baudrate, 8, 'E', 1, timeout=0.25) as ser:
#            frame = b'\xFE\xFE\xFE\xFE\xFE\xFE\x68\x20\xAA\xAA\xAA\xAA\xAA\x11\x11\x01\x03\x3F\x90\x12\xE1\x16'
            build_frame(ser, frame_data)

    except serial.serialutil.SerialException as e:
        print(e)

Only 2400 / 8E1 works as data rate apparently - despite the preamble.

This is the normal data read off my meter:


SEND (022) FE FE FE FE FE FE 68 20 AA AA AA AA AA 11 11 01 03 1F 90 00 AF 16
RECV (064) FE FE FE FE FE 68 20 25 32 26 24 00 00 00 81 2E 1F 90 12 00 00 00 00 05 07 00 00 00 05 00 00 00 00 17 00 00 00 00 35 03 00 00 00 2C 66 25 00 54 25 00 00 31 01 15 12 18 27 04 26 20 00 08 13 16

FE FE FE FE FE        preamble
68 20                 frame start
25 32 26 24 00 00 00  meter address
81                    (response to read ?)
2E                    (length ?)
1F 90 12              (answer to read 901F)
00 00 00 00 05        cooling energy [BCD 00000.000 kWh]
07 00 00 00 05        heating energy [00000.007 kWh]
00 00 00 00 17        current power  [00000.000 kW]
00 00 00 00 35        current flow   [00000.000 m³/h]
03 00 00 00 2C        total volume   [00000.000 m³]
66 25 00              inlet temp (25.66C)
54 25 00              outlet temp (25.54 C)
00 31 01              uptime hours (13100h)
15 12 18 27 04 26 20  timestamp
00 08                 error code
13                    checksum
16                    frame end


The high precision meter reading looks like this:



SEND (022) FE FE FE FE FE FE 68 20 AA AA AA AA AA 11 11 01 03 3F 90 00 CF 16
RECV (076) FE FE FE FE FE 68 25 25 32 26 24 00 00 00 81 3A 3F 90 03 53 25 00 00 00 00 00 00 00 00 2C 00 00 00 00 05 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 10 00 10 00 10 00 10 00 00 00 00 00 00 34 25 00 27 04 26 20 00 08 79 16

FE FE FE FE FE        preamble
68 25                 frame start 25(!)
25 32 26 24 00 00 00  meter address
81                    (response to read?)
3A                    (length)
3F 90 03              (answer to read 903F)
53 25                 (outlet temp)
00 00 00 00           number of pulses
00 00 00 00 2C        cumulative volume [m³]
00 00 00 00 05        cumulate energy [kWh] 
00 00 00              alarm duration [h]
03 00                 nominal diameter
00 00 00 00           zero offset
00 00 00              pulse width
00 00 00 00           (undefined)
00 10                 calibration value 2.50 (0x1000 / 4096 = 1)
00 10                 calibration value 0.75 (0x1000 / 4096 = 1)
00 10                 calibration value 0.25 (0x1000 / 4096 = 1)
00 10                 calibration value 0.05 (0x1000 / 4096 = 1)
00 00 00 00 00 00     (undefined)
34 25 00              inlet temp
27 04 26 20           date
00 08                 error (flow sensor error 0x800)
79                    checksum
16                    end of frame

The broadcast is also only working when all 5 bytes are set to 0xAA and the two upper ones to 0x11; other combinations don’t seem to work, so a collision scan of the bus, when multiple meters are connected, is not an option with those apparently.

From Standard CJ-118-2004:

Code Unit
0x02 Wh
0x05 kWh
0x08 MWh
0x0A 0.1 GWh
0x01 J
0x0B kJ
0x0E MJ
0x11 GJ
0x13 0.1 TJ
0x14 W
0x17 kW
0x1A MW
0x29 dm³
0x2C
0x32 l/h
0x35 m³/h

Status Bits

D0 D1 D2 D3 D4 D5 D6 D7
Definition Battery res res res res res
voltage
Description 0: normal
1: under voltage
D0 D1 D2 D3 D4 D5 D6 D7
Definition Integrator Supply water Return water Flow rate res res res res
breakdown temp sensor temp sensor transducer
Description 0: normal 0: normal 0: normal 0: normal
1: error 1: error 1:error 1: error
Code Nominal Diameter
0x0001 DN15
0x0002 DN20
0x0003 DN25
0x0004 DN32
0x0005 DN40
0x0006 DN50
0x0007 DN65
0x0008 DN80
0x0009 DN100
0x000A DN125
0x000B DN150
0x000C DN200
0x000D DN250
0x000E DN300

LCD Screen values:

Normal Mode (“A0”); → mode change with long button press
Cumulative Heating [0 kWh]
Cumulative Cooling [0 kWh]
Power [0.000 kW]
Cumulative Volume [0.00 m³] —> A3
{Error 4 - Flow sensor fault}
Inlet Temperature [0.00 °C]
Outlet Temperature [0.00 °C]
Differential Temperature [0.00 K]
Uptime [0 h] → A2
-000000- → A1
Serial Number

Internal Settings mode “A1”
-000000-
Serial Number
Voltage (U 2.22)
nominal Diameter (In dn 25)
internal Volume (3.64)
Date (Year-Month-Day)
Time (Hour:Minute:Second)
(long press → A0)

Monthly Energy Use mode “A2”
Year-Month (2024-06)
Heating Energy [kWh]
Cooling Energy [kWh]
Volume [0.00 m³]
Year-Month (2024-05)
Heating Energy [kWh]
Cooling Energy [kWh]
Volume [0.00 m³]
(repeats for all months where the unit was operational)
(when the Date is adjusted via M-Bus, months may be skipped)
{Error 97 - end of data? entry corrupt?}
(long press → A0)

High precision mode “A3”
0.00000 m³
{Error}
Inlet Temp 00.00 °C
Outlet Temp 00.00 °C
Differential Temp 0.00 K
Heating Energy 0.000 kWh
Cooling Energy 0.000 kWh
Power 0.000 kW
(long press → A0)

Values appear to get updated every 8-10 sec followed by a 1-2 sec update, in an alternating pattern.