A generic, transport-independent M-Bus / wM-Bus external component for ESPHome: wireless (CC1101, SX126x, SX127x) and wired (UART) M-Bus in one component core, with a full DIF/VIF/VIFE record dump instead of built-in per-meter drivers.
Note
Project Status: Early stage. Wireless C1 Format A is hardware-validated against a real WaterStarM water meter. Everything else (C1 Format B, T1, SX126x/SX127x, wired UART) is implemented but not yet validated against real hardware β see Supported Transports below.
Please do me a favor: π If you use any information or code you find here, please link back to this page. β Also, please consider to star this project. I really like to keep track of who is using this to do creative things, especially if you are from other parts of the world. π You are welcome to open an issue to report on your personal success project and share it with others.
Other (w)M-Bus options exist for ESPHome; this one is built around a few specific choices:
- Reuses existing ESPHome hardware components. Wireless (CC1101, SX126x, SX127x) and wired (UART) all go through ESPHome's own radio/UART components β no custom hardware drivers to maintain.
- One component, every transport. Wireless and wired share the same core: link framing β application layer β DIF/VIF/VIFE records β ESPHome entities. A TCP transport (M-Bus/wM-Bus gateways) is kept in mind conceptually so the architecture wouldn't have to change to add it, but it is not committed to being built.
- No built-in meter drivers. Instead of a maintained per-model driver database,
esphome-mbusdecodes and dumps the full DIF/VIF/VIFE record structure β raw value, decoded value, unit, storage/tariff/ subunit, function β so you can see exactly what your meter sends and buildsensor:/binary_sensor:/text_sensor:entities for it directly in YAML, without waiting for someone to add your specific meter to a driver list. - No external libraries. Framing, decryption and record decoding are pure ESPHome/ESP-IDF C++ β nothing extra to vendor or version-pin beyond ESPHome itself.
- Transparent scaling. Published sensor values are unscaled; you apply scaling via ESPHome's own
filters:, so nothing is hidden inside a driver.
wireless (CC1101 / SX126x / SX127x) \
wired UART >-- normalized frame --> application layer --> records --> entities
(TCP, conceptual only) /
The mbus component picks exactly one source per instance (radio_cc1101_id, radio_sx126x_id,
radio_sx127x_id, or wired_uart_id); multiple mbus: instances are supported (e.g. several meters on
one link). See components/mbus for the implementation.
This repository temporarily vendors a patched copy of ESPHome core's cc1101 component, under
components/cc1101. It exists only because mbus needs long-packet RX support
(packet_length above 64 bytes, and packet_length_lambda for protocols whose length isn't known
upfront) that core cc1101 doesn't have yet. The patches are split into three upstream pull requests:
- esphome/esphome#17576 β export
CC1101Listenerto Python (not required bymbusitself, but a prerequisite for other external components to usecc1101.CC1101Listener). - esphome/esphome#17577 β FOCCFG/BSCFG frequency offset and bit sync tuning options (not required, improves RX quality for narrowband protocols like wM-Bus).
- esphome/esphome#17578 β long packet RX support
(required by
mbus; this is the actual blocker for dropping the vendored copy).
These pull requests are pending review and may be rejected, changed significantly, or take a while either
way β there's no guarantee they'll be merged as-is, or at all. If and when #17578 lands in an ESPHome
release, mbus can work against core cc1101 directly, the local copy can be removed from this
repository, and cc1101 would no longer need to be listed under components: above. Until then, this
vendored copy remains the way to get long-packet RX support.
| Transport | Implemented | Validated against real hardware |
|---|---|---|
| CC1101, wM-Bus C1 Format A | Yes | Yes (WaterStarM water meter) |
| CC1101, wM-Bus C1 Format B | Yes | No (host-side/synthetic test vectors only) |
| CC1101, wM-Bus T1 | Yes | No (host-side/synthetic test vectors only) |
| SX126x / SX127x, wM-Bus C1/T1 | Yes (shares the CC1101 code path via ESPHome core radio listeners), but capped at 64-byte packets β see below | No |
| Wired M-Bus over UART | Yes | No |
| TCP (M-Bus/wM-Bus gateway) | No, conceptual only, not committed to | β |
external_components:
- source: github://hn/esphome-mbus@main
components: [mbus, cc1101] # drop cc1101 once it's no longer needed, see above
spi:
clk_pin: GPIOXX
mosi_pin: GPIOXX
miso_pin: GPIOXX
cc1101:
id: cc1101_radio
cs_pin: GPIOXX
gdo0_pin: GPIOXX
frequency: 868.95MHz
# ... see the cc1101 documentation for the remaining radio tuning options
mbus:
- id: water_meter
radio_cc1101_id: cc1101_radio
dump_records: true
# meter_id: 0x12345678 # optional: only process frames from this meter's serial number
sensor:
- platform: mbus
mbus_id: water_meter
name: "Total water consumption"
dif: 0x04
vif: 0x13
function: instantaneous
unit_of_measurement: "mΒ³"
device_class: water
state_class: total_increasing
accuracy_decimals: 3
filters:
- multiply: 0.001Instead of radio_cc1101_id, you can use radio_sx126x_id or radio_sx127x_id with a configured
sx126x/sx127x radio component.
Core sx126x/sx127x have the same 64-byte packet cap that cc1101 had, but no equivalent long-packet
patch exists for them here (or upstream) yet. Most wM-Bus telegrams exceed 64 bytes, so the mbus code
path for these radios is implemented and wired up, but likely can't receive full telegrams in practice
until a similar patch is written for them too.
Wired M-Bus over UART works the same way, just with a different source and no radio-specific settings.
secondary_address is the meter's M-Bus secondary address; update_interval controls how often it's
polled (defaults to 60s):
uart:
- id: mbus_uart
tx_pin: GPIOXX
rx_pin: GPIOXX
baud_rate: 2400
parity: EVEN
mbus:
- id: heat_meter
wired_uart_id: mbus_uart
secondary_address: 0x1234567812345678
update_interval: 60s
dump_records: truewatermeter-waterstarm-cc1101.yaml is a full, ready-to-flash
configuration for a real meter (WaterStarM M-ETH Q3 2.5, wireless via CC1101) β the one this component
was originally hardware-validated against. Besides cc1101:/mbus:, it includes WiFi, the native API,
OTA, a captive-portal fallback, and working sensor:/binary_sensor: entities, so it doubles as a
complete reference for wiring everything else in this README together into a real device.
There's no driver database to look your meter up in, so the workflow is to look at what your meter actually sends and match it directly:
- Set
dump_records: trueand flash. Every record your meter sends gets logged as oneRecord:line atDEBUGlevel, showing everything needed to match it:dif,vif(andvif_extif the VIF uses an extension),storage/tariff/subunit,function,vife, plus the raw bytes and the decoded value/unit for context. - Pick the fields for the value you want and copy them into a
sensor:/binary_sensor:/text_sensor:entry (see matching rules below). - Set
dump_records: falseagain once your entities are in place. Formatting and logging every record on every received telegram costs CPU on top of the actual decoding; only turn it back on temporarily when you're adding a new entity or debugging.
Illustrative example, in the shape the real log lines take, for the WaterStarM meter (manufacturer
DWZ) this component was first validated against:
[D][mbus:047]: Received mode C frame with format A: bytes=194 expected=194 l=169 rssi=-80 lqi=51 dFreq=80933
[D][mbus:080]: Block-1: L=169 C=0x44 M=DWZ ID=0x12345678 Version=0x02 DevType=0x07
[D][mbus:083]: CRC: OK, stripped frame bytes=170
[D][mbus:157]: AppHeader: CI=0x7A AccessNr=189 Status=0x00 Config=0x9025 EncMode=5 EncBlocks=9 Encrypted=yes
[D][mbus:167]: Encrypted payload: offset=15 bytes=144
[D][mbus:191]: Decryption: OK, decrypted payload bytes=144
[D][mbus:118]: Record: dif=0x04 vif=0x13 storage=0 tariff=0 subunit=0 function=instantaneous pos=8 vif_raw=0x13 dife=[] vife=[] data_type=int32 raw=A7.BF.01.00 raw_value=114599; decoded: scale=0.001 unit=m3 value=114.599
[D][mbus:118]: Record: dif=0x44 vif=0x13 storage=1 tariff=0 subunit=0 function=instantaneous pos=14 vif_raw=0x13 dife=[] vife=[0x3C] data_type=int32 raw=0C.00.00.00 flags=backward_flow raw_value=12; decoded: scale=0.001 unit=m3 value=0.012
[D][mbus:118]: Record: dif=0x04 vif=0x6D storage=0 tariff=0 subunit=0 function=instantaneous pos=20 vif_raw=0x6D dife=[] vife=[] data_type=datetime_type_f raw=1E.0E.0F.33; decoded: value=2024-03-15T14:30
[D][mbus:118]: Record: dif=0x02 vif=0x7D storage=0 tariff=0 subunit=0 function=instantaneous pos=26 vif_raw=0xFD vif_ext=0x7D17 dife=[] vife=[] data_type=int16 raw=50.00 raw_value=80; decoded: scale=1 unit=flags value=80
The first record line, for example, turns directly into:
sensor:
- platform: mbus
mbus_id: water_meter
name: "Total water consumption"
dif: 0x04
vif: 0x13
function: instantaneous
unit_of_measurement: "mΒ³"
device_class: water
state_class: total_increasing
accuracy_decimals: 3
filters:
- multiply: 0.001 # matches "decoded: scale=0.001" from the dumpThe fourth record line, with its vif_ext=0x7D17 flags/status word, turns into a binary_sensor: picking out
one bit β raw_value=80 is 0b01010000, so bit 4 is set:
binary_sensor:
- platform: mbus
mbus_id: water_meter
name: "Meter battery"
dif: 0x02
vif: 0x7D
vif_ext: 0x7D17
function: instantaneous
bit: 4
device_class: batterydif,vifandfunctionare always compared and must match exactly.functiondefaults toinstantaneousif you don't set it, so there's no wildcard for it β set it explicitly if a record usesmaximum,minimum, orvalue_during_error.vif_ext,storage,tariff,subunitandvifeare optional. If you set one in YAML, the record's value for it must match exactly. If you leave it unset, it's ignored β the entity matches regardless of that field's value in the record.- Every configured
sensor:/binary_sensor:/text_sensor:entry that matches a record gets it; you can have more than one entity match the same record if that's useful. - For
binary_sensor:,bitselects which bit of the record's raw integer value to expose; it isn't part of the record matching itself, just which bit of an already-matched record to publish.
- No meter-, manufacturer-, or model-specific drivers, by design β see Design Philosophy above.
- C1 Format B and T1 need real captured regression fixtures; currently only validated with synthetic/ standards-derived test vectors.
- Wired UART is implemented but frozen pending real hardware logs; response control field, status bits and non-zero signature handling still need hardening before first hardware use.
- TCP transport is only a conceptual placeholder in the architecture; it's not committed to being implemented, by the maintainer or anyone else.
Same as upstream ESPHome: the ESPHome License (MIT and GPL, split by part of the codebase).