In Part 1 you wired the hardware. Now we configure the software. This part covers the two things most guides skip: where to find your Meter ID on the iPerl label, and the default AES-128 key that works for most installations. Then we walk through the complete ESPHome YAML and flash it to the NodeMCU.
Before you start:
— Hardware from Part 1 assembled (NodeMCU V3 + E07-900M10S wired up)
— Home Assistant running with the ESPHome add-on installed
— Physical access to the meter, or willingness to flash with
— Hardware from Part 1 assembled (NodeMCU V3 + E07-900M10S wired up)
— Home Assistant running with the ESPHome add-on installed
— Physical access to the meter, or willingness to flash with
log_unknown: True firstStep 1 — Find your Meter ID
The Meter ID uniquely identifies your water meter. The
wmbus component uses it to filter your meter’s telegrams from all the others transmitting nearby — in an apartment building there can be a dozen meters on the same frequency simultaneously. Two ways to find it:Method 1 — label on the meter (faster)
On the Sensus iPerl housing, in the top right corner, there is a serial number starting with 8 SEN. The digits after those characters are your Meter ID.
Example label:
8 SEN 2086 3703
Label: 8 SEN 2086 3703
YAML: meter_id: “0x20863703”
Method 2 — log_unknown in ESPHome (when label is unreadable)
Flash the firmware with
meter_id: "0x00000000" and log_unknown: true. ESPHome will log every wMBus telegram it receives, including the Meter ID for each one.What you will see in the ESPHome Dashboard log:
[D][wmbus:176]: Meter ID [0x20863703] RSSI: -56 dBm LQI: 128
^^^^^^^^^^^
This is your Meter ID
The ID that appears regularly is your meter. Neighbours’ meters will also appear but each has a different ID. Note: logs are only visible in the ESPHome Dashboard Logs tab — not in the web panel at
water-meter-nmc.local.Step 2 — AES-128 encryption key
wMBus telegrams are AES-128 encrypted. Without the correct key ESPHome receives the signal but cannot read any values — it sees only an encrypted byte stream.
Default factory key — Sensus iPerl
Most utility companies do not change this key. If your meter has its original factory configuration, this works immediately:
Default AES-128 key — Sensus iPerl:
E6C88800DEB868C0D6A84880CE982840
If the key does not work:
No
No
2F2F check after decrypting - OK in the log means the key was changed by your utility. Request it from your building manager or water company — as the meter owner you are entitled to it.Step 3 — New device in ESPHome Dashboard
1
In ESPHome Dashboard click + New Device
2
Enter device name e.g.
water-meter-nmc — lowercase, no spaces3
Select platform ESP8266
4
Click Edit on the new device and replace the entire content with the YAML below
Step 4 — ESPHome YAML configuration
Never put passwords in YAML — use secrets.yaml
Store credentials in
Store credentials in
/config/esphome/secrets.yaml:
wifi_ssid: "YourNetworkName" wifi_password: "YourWiFiPassword" ota_password: "AnyOTAPassword"
The YAML below reads from secrets.yaml automatically using !secret syntax.
Two-step process for finding Meter ID:
Step 1: Flash with
Step 2: Fill in your Meter ID, set
Step 1: Flash with
meter_id: "0x00000000" and log_unknown: true. Open ESPHome Dashboard Logs and wait for a telegram.Step 2: Fill in your Meter ID, set
log_unknown: false, reflash via OTA.Important: The iPerl transmission interval is set by your water utility — typically 10–30 minutes during the day and much less frequent at night. Do not assume something is broken if the log is silent for 15–30 minutes. Just wait.
## ------------------------------------------------------------------
## WMBUS NodeMCU V3 + EBYTE E07-900M10S (CC1101 868MHz)
## Supports 30 wMBus meter types (water, gas, heat, electricity)
## External component: github.com/SzczepanLeon/esphome-components
## Credits: based on ha-watermeter by Peter Siebler (zibous)
## ------------------------------------------------------------------
substitutions:
device_name_short: "water-meter-nmc"
friendly_name: "Watermeter"
appversion: "3.0.0"
log_level: "WARN" # Production: WARN | Debugging: VERY_VERBOSE
log_wmbus: "WARN" # Production: WARN | Debugging: VERY_VERBOSE
log_baudrate: "0" # 0 = disable UART logging
esp8266:
board: nodemcuv2
restore_from_flash: true
esphome:
name: ${device_name_short}
on_boot:
then:
- globals.set:
id: boot_counter
value: !lambda "return id(boot_counter)+=1;"
- globals.set:
id: send_millisecond
value: !lambda "return millis();"
external_components:
- source: github://SzczepanLeon/esphome-components@version_3
refresh: 0s
components: [wmbus]
logger:
level: ${log_level}
baud_rate: ${log_baudrate}
logs:
wmbus: ${log_wmbus}
wMBus-lib: ${log_wmbus}
wmbus.unknown: ${log_wmbus} # Required for log_unknown mode
sensor: WARN
sensor.filter: WARN
text_sensor: WARN
api.service: ERROR
json: ERROR
wifi: WARN
component: ERROR
api: WARN
globals:
- id: boot_counter
type: int
restore_value: yes
initial_value: "0"
- id: last_value
type: float
restore_value: yes
initial_value: "0.00"
- id: current_value
type: float
restore_value: yes
initial_value: "0.00"
- id: hour_value
type: float
restore_value: yes
initial_value: "0.00"
- id: daily_value
type: float
restore_value: yes
initial_value: "0.00"
- id: yesterday_value
type: float
restore_value: yes
initial_value: "0.00"
- id: week_value
type: float
restore_value: yes
initial_value: "0.00"
- id: lastmonth_value
type: float
restore_value: yes
initial_value: "0.00"
- id: year_value
type: float
restore_value: yes
initial_value: "0.00"
- id: send_millisecond
type: int
restore_value: false
initial_value: "0"
- id: cc1101_state
type: int
restore_value: no
initial_value: "0"
- id: cc1101_state_message
type: std::vector<std::string>
restore_value: no
initial_value: '{"Init", "Waiting for data", "Data received", "Ready", "Error"}'
- id: alarm_error_text
type: std::vector<std::string>
restore_value: no
initial_value: '{"No alarm", "General alarm", "Leakage", "Meter blocked", "Back flow", "Underflow", "Overflow", "Submarine", "Sensor fraud", "Mechanical fraud"}'
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
mdns:
disabled: false
captive_portal:
ota:
password: !secret ota_password
platform: esphome
web_server:
port: 80
version: 2
api:
reboot_timeout: 0s
services:
- service: set_watermeter_esp_data
variables:
water_val_hour: float
water_val_day: float
water_val_yesterday: float
water_val_week: float
water_val_year: float
water_val_lastmonth: float
then:
- globals.set:
id: hour_value
value: !lambda |-
if((water_val_hour) and (water_val_hour) > 0.001){ return (water_val_hour); }
return id(hour_value);
- globals.set:
id: daily_value
value: !lambda |-
if((water_val_day) and (water_val_day) > 0.001){ return (water_val_day); }
return id(daily_value);
- globals.set:
id: yesterday_value
value: !lambda |-
if((water_val_yesterday) and (water_val_yesterday) > 0.001){ return (water_val_yesterday); }
return id(yesterday_value);
- globals.set:
id: week_value
value: !lambda |-
if((water_val_week) and (water_val_week) > 0.001){ return (water_val_week); }
return id(week_value);
- globals.set:
id: lastmonth_value
value: !lambda |-
if((water_val_lastmonth) and (water_val_lastmonth) > 0.001){ return (water_val_lastmonth); }
return id(lastmonth_value);
- globals.set:
id: year_value
value: !lambda |-
if((water_val_year) and (water_val_year) > 0.001){ return (water_val_year); }
return id(year_value);
time:
- platform: sntp
id: time_sntp
timezone: Europe/Warsaw # Change to your timezone
servers:
- 0.pool.ntp.org
on_time_sync:
then:
- if:
condition:
lambda: 'return id(device_lastBoot_time).state == "";'
then:
- text_sensor.template.publish:
id: device_lastBoot_time
state: !lambda return id(time_sntp).now().strftime("%Y-%m-%dT%H:%M:%S %Z");
- script.execute: set_status_message
on_time:
- seconds: 30
then:
- script.execute: set_status_message
- seconds: 0
minutes: 0
then:
- globals.set:
id: hour_value
value: "0.00"
- lambda: id(waterhour).publish_state(id(hour_value));
- seconds: 0
minutes: 0
hours: 0
then:
- lambda: |-
id(yesterday_value) = id(daily_value);
id(wateryesterday).publish_state(id(yesterday_value));
id(daily_value) = 0.00;
id(waterday).publish_state(id(daily_value));
- seconds: 0
minutes: 0
hours: 0
days_of_week: MON
then:
- globals.set:
id: week_value
value: "0.00"
- lambda: id(waterweek).publish_state(id(week_value));
- seconds: 0
minutes: 0
hours: 0
days_of_month: 1
months: JAN
then:
- globals.set:
id: year_value
value: "0.00"
- lambda: id(wateryear).publish_state(id(year_value));
script:
- id: set_status_message
then:
- lambda: |-
int msgcode = int(id(cc1101_state));
std::string message = id(cc1101_state_message)[msgcode];
id(watermeter_status_message).publish_state(message);
## ------------------------------------------------------------------
## WMBUS — CC1101 pin config (hardware SPI — do not change)
## ------------------------------------------------------------------
wmbus:
mosi_pin: GPIO13 # D7
miso_pin: GPIO12 # D6
clk_pin: GPIO14 # D5
cs_pin: GPIO15 # D8 — 10k pull-down is intentional
gdo0_pin: GPIO05 # D1
gdo2_pin: GPIO04 # D2
log_unknown: true # Set false after finding your Meter ID
## Supported meter types — change type: iperl to match your meter:
## WATER: iperl, izar, itron, apator08, apator162, apatoreitn,
## hydrus, hydrocalm3, multical21, qwater, ultrimis,
## unismart, flowiq2200, rfmtx1, mkradio3, mkradio4, mkradio4a
## HEAT: kamheat, qheat, sharky774, fhkvdataiii, vario451, topaseskr
## GAS/E: amiplus, bmeters, c5isf, compact5, dme07, elf, evo868
switch:
- platform: template
name: "${friendly_name} Reset Water Values"
turn_on_action:
then:
- lambda: |-
id(last_value) = 0.00; id(boot_counter) = 0;
id(bootcounter).publish_state(id(boot_counter));
id(current_value) = 0.00; id(watercurrent).publish_state(0);
id(hour_value) = 0.00; id(waterhour).publish_state(0);
id(daily_value) = 0.00; id(waterday).publish_state(0);
id(yesterday_value) = 0.00; id(wateryesterday).publish_state(0);
id(week_value) = 0.00; id(waterweek).publish_state(0);
id(lastmonth_value) = 0.00; id(waterlastmonth).publish_state(0);
id(year_value) = 0.00; id(wateryear).publish_state(0);
- platform: restart
name: "${friendly_name} Restart"
id: restart_switch
sensor:
- platform: wmbus
## -----------------------------------------------------------
## Step 1: Flash with 0x00000000 + log_unknown: true
## Step 2: Find your ID in log (look for [wmbus:176] line)
## Step 3: Fill in ID below, set log_unknown: false, reflash
## -----------------------------------------------------------
meter_id: "0x00000000" # <-- YOUR METER ID HERE
type: iperl # Change for other meter types (see list above)
key: "E6C88800DEB868C0D6A84880CE982840"
add_prefix: true
lqi:
id: wmbus_cc1101_lqi
name: "${friendly_name} CC1101 LQI"
entity_category: "diagnostic"
unit_of_measurement: "lqi"
state_class: "measurement"
rssi:
id: wmbus_cc1101_rssi
total_water_m3:
id: "waterdisplay"
name: "Water Meter Display"
unit_of_measurement: "m3"
state_class: total_increasing
device_class: "water"
accuracy_decimals: 3
icon: mdi:counter
on_value:
then:
- lambda: |-
if ((id(last_value) > 0.00) and (id(waterdisplay).state) > (id(last_value))) {
id(cc1101_state) = 2;
id(current_value) = float(id(waterdisplay).state - id(last_value)) * 1000.00;
id(watercurrent).publish_state(id(current_value));
id(hour_value) += id(current_value); id(waterhour).publish_state(id(hour_value));
id(daily_value) += id(current_value); id(waterday).publish_state(id(daily_value));
id(week_value) += id(current_value); id(waterweek).publish_state(id(week_value));
id(year_value) += id(current_value); id(wateryear).publish_state(id(year_value));
} else {
id(current_value) = 0.00; id(cc1101_state) = 1;
id(watercurrent).publish_state(id(current_value));
}
id(last_value) = id(waterdisplay).state;
- text_sensor.template.publish:
id: watermeter_lastupdate
state: !lambda return id(time_sntp).now().strftime("%Y-%m-%dT%H:%M:%S %Z");
- sensor.template.publish:
id: watermeter_read_timeout
state: !lambda |-
int time_used = (millis() - id(send_millisecond));
id(send_millisecond) = millis();
return float(time_used) / 1000;
- script.execute: set_status_message
- binary_sensor.template.publish:
id: statusled
state: !lambda "return id(current_value) > 0;"
last_month_total_water_m3:
name: "Water Meter Last Month Display"
id: "waterdisplay_lastmonth"
unit_of_measurement: "m3"
state_class: total_increasing
device_class: "water"
accuracy_decimals: 3
current_month_total_water_l:
name: "Water Month"
id: "watermonth"
accuracy_decimals: 2
unit_of_measurement: "L"
state_class: total_increasing
device_class: "water"
remaining_battery_life_y:
name: "Water Meter Battery Life"
id: "watermeter_batterie"
accuracy_decimals: 2
unit_of_measurement: "years"
entity_category: "diagnostic"
icon: mdi:battery
transmit_period_s:
name: "Water Meter Transmit Interval"
id: "watermeter_transmit_periode"
unit_of_measurement: "s"
accuracy_decimals: 0
entity_category: "diagnostic"
icon: mdi:timelapse
current_alarms:
id: "watermeter_current_alarms"
name: "Water Meter Alarm Code"
entity_category: "diagnostic"
on_value:
then:
- lambda: |-
int error_code = int(x);
std::string message = id(alarm_error_text)[error_code];
id(watermeter_alarm_message).publish_state(message);
if(error_code != 0){
id(watermeter_alarm_timestamp).publish_state(
id(time_sntp).now().strftime("%Y-%m-%dT%H:%M:%S %Z").c_str());
}
previous_alarms:
id: "watermeter_previous_alarms"
name: "Water Meter Previous Alarm Code"
entity_category: "diagnostic"
on_value:
then:
- lambda: |-
int error_code = int(x);
id(watermeter_alarm_perv_message).publish_state(id(alarm_error_text)[error_code]);
if(!id(watermeter_alarm_timestamp).has_state())
id(watermeter_alarm_timestamp).publish_state("--");
- platform: template
name: "Water Current"
id: "watercurrent"
accuracy_decimals: 2
unit_of_measurement: "L"
state_class: "measurement"
device_class: "water"
lambda: return (id(current_value));
- platform: template
name: "Water Hour"
id: "waterhour"
accuracy_decimals: 2
unit_of_measurement: "L"
state_class: total_increasing
device_class: "water"
lambda: return (id(hour_value));
- platform: template
name: "Water Today"
id: "waterday"
accuracy_decimals: 2
unit_of_measurement: "L"
device_class: "water"
state_class: total_increasing
lambda: return (id(daily_value));
- platform: template
name: "Water Yesterday"
id: "wateryesterday"
accuracy_decimals: 2
unit_of_measurement: "L"
device_class: "water"
state_class: total_increasing
lambda: return (id(yesterday_value));
- platform: template
name: "Water Week"
id: "waterweek"
accuracy_decimals: 2
unit_of_measurement: "L"
device_class: "water"
state_class: total_increasing
lambda: return (id(week_value));
- platform: template
name: "Water Year"
id: "wateryear"
accuracy_decimals: 2
unit_of_measurement: "L"
state_class: total_increasing
device_class: "water"
lambda: return (id(year_value));
- platform: template
name: "Water Last Month"
id: "waterlastmonth"
accuracy_decimals: 2
unit_of_measurement: "L"
state_class: total_increasing
device_class: "water"
lambda: return (id(lastmonth_value));
- platform: template
name: "Water Meter Data Interval"
id: watermeter_read_timeout
icon: mdi:clock-start
accuracy_decimals: 1
unit_of_measurement: "s"
state_class: "measurement"
entity_category: "diagnostic"
- platform: wifi_signal
id: wifi_signal_db
update_interval: 60s
internal: true
disabled_by_default: true
- platform: copy
source_id: wifi_signal_db
name: "${friendly_name} WiFi Quality"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "%"
entity_category: "diagnostic"
- platform: copy
source_id: wmbus_cc1101_rssi
name: "${friendly_name} CC1101 Signal Quality"
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "%"
entity_category: "diagnostic"
- platform: uptime
name: "${friendly_name} Uptime"
id: uptime_human
filters:
- lambda: return x / 3600;
unit_of_measurement: "h"
entity_category: "diagnostic"
state_class: "measurement"
accuracy_decimals: 2
- platform: template
name: "${friendly_name} Boot Counter"
id: bootcounter
icon: mdi:counter
accuracy_decimals: 0
state_class: "measurement"
entity_category: "diagnostic"
lambda: return (id(boot_counter));
binary_sensor:
- platform: template
name: "Water Meter Status LED"
id: statusled
icon: mdi:water
entity_category: "diagnostic"
lambda: "return id(current_value) > 0;"
text_sensor:
- platform: template
name: "Water Meter Status"
id: watermeter_status_message
icon: mdi:information
entity_category: "diagnostic"
- platform: template
name: "Water Meter Alarm"
id: watermeter_alarm_message
icon: mdi:alarm-light
entity_category: "diagnostic"
- platform: template
name: "Water Meter Previous Alarm"
id: watermeter_alarm_perv_message
icon: mdi:alarm-light
entity_category: "diagnostic"
- platform: template
name: "Water Meter Alarm Time"
id: watermeter_alarm_timestamp
icon: mdi:clock-alert
entity_category: "diagnostic"
- platform: version
name: "${friendly_name} ESPHome Version"
id: appver
disabled_by_default: true
- platform: template
name: "${friendly_name} Last Boot"
disabled_by_default: true
id: device_lastBoot_time
icon: mdi:clock-start
- platform: template
name: "${friendly_name} Last Update"
id: watermeter_lastupdate
icon: mdi:clock-check
entity_category: "diagnostic"
- platform: template
id: systime
entity_category: "diagnostic"
internal: true
disabled_by_default: true
lambda: return id(time_sntp).now().strftime("%Y-%m-%dT%H:%M:%S %Z");
- platform: template
id: watermeterdata
internal: true
disabled_by_default: true
lambda: |-
char buf[128];
sprintf(buf, "%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%.3f|%s",
id(waterdisplay).state, id(current_value), id(hour_value),
id(daily_value), id(yesterday_value), id(week_value),
id(watermonth).state, id(lastmonth_value), id(year_value),
id(systime).state.c_str());
std::string s = buf;
return s;
Step 5 — Flashing and reading the log
1
Connect NodeMCU via USB to a computer running Chrome or Edge
2
In ESPHome Dashboard: three dots next to your device → Install → Plug into this computer
3
Wait for compilation (~2–5 min first time) and flashing. After reboot the device connects to WiFi and appears in Home Assistant.
4
Open Logs in ESPHome Dashboard to see incoming telegrams. Temporarily set
log_level: "VERY_VERBOSE" for this step.Phase 1 — Discovering the Meter ID (meter_id: 0x00000000)
[10:02:50][V][rxLoop:167]: Have 56 bytes from CC1101 Rx, RSSI: -56 dBm LQI: 128
[10:02:50][D][mbus:034]: Processing T1 A frame
[10:02:50][V][crc:031]: calculated: 0x5FCB, read: 0x5FCB <-- Block1 OK
[10:02:50][V][crc:031]: calculated: 0x38CD, read: 0x38CD <-- Block2 OK
[10:02:50][V][crc:031]: calculated: 0xB521, read: 0xB521 <-- Block3 OK
[10:02:50][D][wmbus:176]: Meter ID [0x20863703] RSSI: -56 dBm LQI: 128
^^^^^^^^^^^
Copy this value to meter_id in your YAML
"not found in configuration" at this stage is normal
Once you have the ID: fill it in as
meter_id: "0x20863703", set log_unknown: false, and reflash via OTA (no USB needed). Then wait — the interval may be 10–30 minutes.Phase 2 — Working reception (Meter ID configured)
[14:02:52][V][rxLoop:167]: Have 56 bytes from CC1101 Rx, RSSI: -50 dBm LQI: 128
[14:02:52][D][mbus:034]: Processing T1 A frame
[14:02:52][V][crc:031]: calculated: 0x5FCB, read: 0x5FCB <-- Block1 OK
[14:02:52][V][crc:031]: calculated: 0x5C7E, read: 0x5C7E <-- Block2 OK
[14:02:52][V][crc:031]: calculated: 0x3A78, read: 0x3A78 <-- Block3 OK
[14:02:52][D][wmbus:090]: Using driver 'iperl' for ID [0x20863703] RSSI: -50 dBm
^^^^^^^
driver matched -- meter type recognised
[14:02:52][V][utils:291]: 2F2F check after decrypting - OK
^^
AES key correct -- most important confirmation line
[14:02:52][VV][drivers:253]: Found register '0413' with '836236'->'836.236000'
[14:02:52][V][wmbus:121]: Publishing 'total_water_m3' = 836.2360
^^^^^^^^^
value sent to Home Assistant
Key lines explained:
[rxLoop:167]
CC1101 received a radio frame. RSSI -50 dBm is excellent signal.
[mbus:034]
T1 A frame — transmission mode used by iPerl. Correct frequency confirmed.
[crc:031]
CRC validation — iPerl sends 3 data blocks, each with its own CRC. All three must match.
[wmbus:090]
Driver matched — only appears after the correct Meter ID is configured.
[utils:291]
2F2F check OK — the most important line. AES key correct, telegram decrypted. If missing: wrong key.
[wmbus:121]
Value published to HA — entity changes from unknown to real reading.
Not using iPerl? 30 meter types supported
The
wmbus component supports 30 different wMBus meter types. Just change type: iperl to the correct type for your meter: Water meters
iperlizaritronapator08apator162apatoreitnhydrushydrocalm3multical21qwaterultrimisunismartflowiq2200rfmtx1mkradio3mkradio4mkradio4a Heat and cooling meters
kamheatqheatsharky774fhkvdataiiivario451topaseskr Gas and electricity meters
amiplusbmetersc5isfcompact5dme07elfevo868Full and up-to-date driver list: github.com/SzczepanLeon/esphome-components
What’s next — Part 3
The device is running and sending data to Home Assistant. In Part 3 we build the HA dashboard — consumption graphs, daily and weekly charts, leak detection alerts, and billing statistics.
Source code on GitHub:
Complete firmware and instructions at github.com/marklabspl
Complete firmware and instructions at github.com/marklabspl

![Sensus iPerl ESPHome Setup — Meter ID, AES Key & Full YAML Config [Part 2] 2](https://marklabs.pl/wp-content/uploads/2026/05/sensus-iperl-home-assistant-logs-1.png)
![Sensus iPerl ESPHome Setup — Meter ID, AES Key & Full YAML Config [Part 2] 3](https://marklabs.pl/wp-content/uploads/2026/05/sensus-iperl-home-assistant-logs-console-1.png)