06 · YAML Cookbook
This chapter opens with the flagship scenarios — the real-world deployment
patterns the component was designed for in v1.3 (mains + N sub-loads on one bus
with fleet_gc_enable synchronization, provisioning workflow, multi-channel
mixed-CT, fleet GC sync). After those come the single-module scenarios, from a
minimal UI1 to a production deployment with HA automations.
Each example is self-contained: copy it, fill in your board name and Wi-Fi credentials, compile, and flash. Wherever an example departs from the minimal template, an explanation tells you why.
Cross-references:
- Component schema, register map, period-metering protocol, multi-module topology, fleet GC sync, and the NACK loose-sanity rule: 09 · API reference
| # | Scenario | Topology | When to use |
|---|---|---|---|
| 1 | Mains + N sub-loads — the 80% canon | UI1 + N× I2/I3 | Whole-home metering with a breakdown by sub-branches |
| 2 | Provisioning workflow (virgin → fleet) | sequential single | Bring-up of a new batch of modules |
| 3 | Multi-channel mixed-CT (UI3 with different clamps) | UI3 | Metering of mixed loads with accuracy optimization |
| 4 | Fleet GC sync (billing-grade synchrony) | N× UI/I | Multi-tenant clusters or billing applications |
| 5 | UI1 — single-channel (minimal) | UI1 | Hello-world single-module |
| 6 | UI1 with customization | UI1 | All the standard ESPHome knobs |
| 7 | UI3 — three-channel with mixed clamps | UI3 | Single-module multi-channel |
| 8 | Multi-module bus (legacy sequential) | N× UI/I | No GC — fallback to sequential latch |
| 9 | One-time I²C address change | single | Provisioning a single module |
| 10..20 | HA integrations and automations | mixed | Production deployment patterns |
Scenarios 1-4 are the flagship ones, targeting the canonical v1.3 deployment of the package. Their code uses
fleet_gc_enable+ identity surface + read-compare-write and is validated on bench hardware (a heterogeneous UI1+I2+I3 fleet). Scenarios 5-20 are single-module and legacy compose patterns. Scenario 8 is the deprecated per-module sequential latch without fleet_gc, kept for comparison and as a migration path.
1 · Mains + N sub-loads (the 80% canon) — integrated metering system
Goal: the canonical deployment of the package — a coherent metering system on a heterogeneous fleet, closed within a single ESPHome boot (discovery → configure → enable GC → loop[publish_state + auto-emit GC tick]).
Hardware (HW-validated on the Fix-A fleet): UI1@0x50 + I2@0x51 + I3@0x52; ~0.58 A load; an external 4.7 kΩ pull-up on SDA + SCL.
esphome:
name: home-fleet
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
# Three rbAmp modules on shared bus, all with fleet_gc_enable + group_id=1
# for synchronized period latching (billing-grade sync between mains + sub-meters).
rbamp:
- id: mains_meter # UI1 on the mains feed
address: 0x50
sensor_class: SCT_013
ct_model: SCT_013_030
fleet_gc_enable: true
group_id: 1
- id: boiler_meter # I2 on the boiler + washing machine
address: 0x51
sensor_class: SCT_013
ct_models: [SCT_013_020, SCT_013_010]
fleet_gc_enable: true
group_id: 1
- id: panel_meter # I3 on the distribution-panel branches
address: 0x52
sensor_class: SCT_013
ct_models: [SCT_013_005, SCT_013_030, SCT_013_020]
fleet_gc_enable: true
group_id: 1
sensor:
- platform: rbamp
rbamp_id: mains_meter
voltage: { name: "Mains Voltage" }
current: { name: "Mains Current" }
power: { name: "Mains Power" }
power_factor: { name: "Mains PF" }
energy: { name: "Mains Energy" }
frequency: { name: "Mains Frequency" }
- platform: rbamp
rbamp_id: boiler_meter
current: { name: "Boiler Current" } # 20A CT
current_1: { name: "Washer Current" } # 10A CT
energy: { name: "Boiler Energy" } # current-only → only Wh if available
energy_1: { name: "Washer Energy" }
- platform: rbamp
rbamp_id: panel_meter
current: { name: "Lighting Current" } # 5A CT
current_1: { name: "Appliances Current" } # 30A CT
current_2: { name: "Sub-panel Current" } # 20A CT
# Optional: HA template sensor for total household power via lambda
sensor:
- platform: template
name: "Household Total Power"
update_interval: 60s
accuracy_decimals: 0
unit_of_measurement: "W"
device_class: power
lambda: |-
// Sum of power from the mains meter only (sub-meters are a subset of the same mains)
auto v = id(mains_power_state);
return v.has_state() ? v.state : 0.0f;Bench output (HW-validated, ~0.58 A load)
[I][i2c:136]: Found i2c device at address 0x50
[I][i2c:136]: Found i2c device at address 0x51
[I][i2c:136]: Found i2c device at address 0x52
[C][rbamp:450]: mains_meter: variant=UI1, capability=0x0718, fleet_gc=on, group=1
[C][rbamp:450]: boiler_meter: variant=I2, capability=0x0618, fleet_gc=on, group=1
[C][rbamp:450]: panel_meter: variant=I3, capability=0x069E, fleet_gc=on, group=1
[I][rbamp:493]: GC latch enabled fleet-wide: 3 modules in group 1
ITER 1 @ 60s: GC tick=1, 3/3 in_sync
mains: V=232.6 I=5.79 P=1259 W E_total: 0 → 0.35 Wh
boiler: I=[11.58, 3.87] ; current-only
panel: I=[3.84, 2.55, 5.84] ; current-onlyNotes for use
- One synchronized loop — the whole system. On every
update_intervaltick, the ESP32 emits a GC frame to 0x00 → all 3 modules latch their period accumulators at the same wire moment (validated 3/3 in_sync under load). mains_powerin a heterogeneous fleet = the active power from UI1 (mains_meter). The sub-meters (I2/I3) are current-only, with no canonical P. In the 80% canon,mains_power≈ the total power of all loads (mains_meter covers the feed).- Wh = master wall-clock (L9 canon). The ESPHome component integrates via
millis(), NOT via the chip-sideREG_PERIOD_LATCH_MS. Validated on the bench: master_dt/wall=0.999 vs latch_ms/wall=0.743 (chip undercount ~27%, see 10_troubleshooting.md §3). - MISS-resilient. If one module goes off-bus (provisioning / power cycle), the rest keep working; HA shows "Unavailable" for its entities until it recovers.
2 · Provisioning workflow (virgin → fleet)
Goal: the canonical bring-up procedure for new modules, from the factory
0x50 to unique fleet addresses. It demonstrates the two-phase address commit
(v1.3 production-OK) and the required discipline of "one virgin on the bus at a
time".
Step 2.1 — provision UI1 mains (0x50 → 0x50, sensor_class + ct_model)
# Connect ONLY the UI1 to the bus. Flash this YAML.
rbamp:
- id: provisioning_target
address: 0x50
# new_address not needed — 0x50 is already fine for UI1
sensor_class: SCT_013
ct_model: SCT_013_030
fleet_gc_enable: true
group_id: 1
sensor:
- platform: rbamp
rbamp_id: provisioning_target
voltage: { name: "Provision UI1 Voltage" }
current: { name: "Provision UI1 Current" }
text_sensor:
- platform: template
name: "Provision UI1 variant"
update_interval: 5s
lambda: 'return id(provisioning_target).get_variant_str();'
- platform: template
name: "Provision UI1 UID"
update_interval: 5s
lambda: 'return id(provisioning_target).get_uid_hex();'
- platform: template
name: "Provision UI1 capability"
update_interval: 5s
lambda: 'return id(provisioning_target).get_capability_hex();'Check: in HA you should see sensor.provision_ui1_variant == "UI1" +
sensor.provision_ui1_capability == "0x0718" (CAP_GC_LATCH +
CAP_TWO_PHASE_ADDR + CAP_SAVE_USER_CONFIG + CAP_CLEAR_ERROR). Record the UID for
inventory tracking.
Step 2.2 — provision I2 (0x50 → 0x51 via two-phase commit)
Disconnect the UI1 from the bus (otherwise there will be an address conflict). Connect the I2 module. Flash the updated YAML:
rbamp:
- id: provisioning_target
address: 0x50 # virgin address
new_address: 0x51 # ⚠ two-phase commit — production-OK on v1.3
sensor_class: SCT_013
ct_models: [SCT_013_020, SCT_013_010]
fleet_gc_enable: true
group_id: 1What happens at boot:
[I][rbamp:xxx]: provisioning_target: probe at 0x50 → OK (variant=I2)
[I][rbamp:xxx]: provisioning_target: new_address=0x51 requested
[I][rbamp:xxx]: provisioning_target: CAP_TWO_PHASE_ADDR set → production path
[D][rbamp:xxx]: provisioning_target: stage REG_I2C_ADDRESS=0x51
[D][rbamp:xxx]: provisioning_target: write REG_ADDR_COMMIT_MAGIC=0xA5
[D][rbamp:xxx]: provisioning_target: send CMD_COMMIT_ADDR → wait 700ms
[I][rbamp:xxx]: provisioning_target: re-probe 0x51 → OK, re-enumeration complete
[I][rbamp:xxx]: provisioning_target: i2c address handle updated to 0x51After success: update the YAML to address: 0x51 and remove
new_address:. The next boot will not run the change flow.
Step 2.3 — provision I3 (0x50 → 0x52) + repeat for the rest
Same pattern as 2.2: disconnect the I2, connect the virgin I3, flash the YAML
with new_address: 0x52, wait for re-enumeration, and update the YAML to
address: 0x52.
🛑 MUST: one virgin on the bus at a time. If two virgin modules are both on 0x50, both will respond on the address phase and drive SDA against each other → indeterminate state, and no provisioning procedure will work.
Step 2.4 — assemble the fleet
Once provisioning of all modules is complete, update the YAML to the final fleet config (see scenario §1 above), disable the internal pull-ups on all modules except one (see 04_hardware.md) + add an external 4.7 kΩ pair. Flash the final YAML — all modules should be discovered:
[D][i2c:136]: Found i2c device at address 0x50
[D][i2c:136]: Found i2c device at address 0x51
[D][i2c:136]: Found i2c device at address 0x52Failure modes (provisioning)
| Symptom | Cause | Recovery |
|---|---|---|
Probe failed at 0x50 after flash |
Module not powered or a broken contact | Check VCC + GND + SDA + SCL connectivity |
[W] CAP_TWO_PHASE_ADDR not in capability |
Module on legacy firmware (v1.0-v1.2) | Single-phase fallback requires factory-provisioning mode (see 04_hardware.md) |
[E] address change committed but re-probe at 0x51 failed |
Marginal bus or aborted commit | Retry the boot; if it repeats, the external pull-up is too weak or there's an EMI burst |
[W] WARN_FACTORY_MODE_REQUIRED |
Legacy firmware without CAP_TWO_PHASE_ADDR | The supplier enables factory mode (out of scope for the library) |
3 · Multi-channel mixed-CT (UI3 with different clamps)
Goal: to show that the CT model on different channels of a single module can
be mixed according to the load characteristic. The A1 canon is descending
iteration with order-independent bind, validated on the bench (mixed codes
[1, 3, 6] → mirrors 01 03 06, no clobber).
rbamp:
- id: panel_meter
address: 0x52
sensor_class: SCT_013
# Mixed-CT: small/medium/large on channels 0/1/2
ct_models: [SCT_013_005, SCT_013_030, SCT_013_020]
# Order validated on the bench:
# - ch2 binds first (SCT_013_020 = code 6) — REG_CT_MODEL=6 → CMD_SET_CT_MODEL_CH2 → SAVE
# - ch1 next (SCT_013_030 = code 3) — REG_CT_MODEL=3 → CMD_SET_CT_MODEL_CH1 → SAVE
# - ch0 settles last (SCT_013_005 = code 1) — REG_CT_MODEL=1 → CMD_SET_CT_MODEL_CH0 → SAVE
# Post-boot mirrors: REG_CT_MODEL_CH0=1, CH1=3, CH2=6 — no clobber
fleet_gc_enable: true
group_id: 1
sensor:
- platform: rbamp
rbamp_id: panel_meter
current: { name: "Panel Lighting (5A CT)" } # ch0
current_1: { name: "Panel Appliances (30A CT)" } # ch1
current_2: { name: "Panel Boiler (20A CT)" } # ch2
# No voltage / power — this is an I3 SKU, current-only
text_sensor:
- platform: template
name: "Panel last error"
update_interval: 30s
lambda: 'return id(panel_meter).get_last_error_str();'
# → "OK" in a healthy state; "ERR_PARAM" if a CT model code is rejectedVerify boot-time CT binding
After flash + boot, in the log:
[D][rbamp:xxx]: panel_meter: REG_CT_MODEL_CH0 mirror = 1 (SCT_013_005)
[D][rbamp:xxx]: panel_meter: REG_CT_MODEL_CH1 mirror = 3 (SCT_013_030)
[D][rbamp:xxx]: panel_meter: REG_CT_MODEL_CH2 mirror = 6 (SCT_013_020)
[I][rbamp:xxx]: panel_meter: CT binding verified, all mirrors match YAML request
[C][component:246]: Setup panel_meter took ~2100ms (cold install: 3 × 700ms flash + verify)On a warm boot (mirrors match), with no flash write:
[I][rbamp:xxx]: panel_meter: CT_MODEL[0..2] match YAML → SKIP flash erase
[C][component:246]: Setup panel_meter took 72ms (warm boot: read-compare only)Bench-validated facts
- Mixed CT binding: codes [1, 3, 6] on the bench (Fix-A I3 @ 0x52) → mirrors
01 03 06, no clobber. - Reserved code rejection: an attempt at
ct_models: [SCT_013_100, ...](code 5) →esphome configrejects the schema (cv.Invalid). If the YAML slips past the validator (manual edit / legacy build), the firmware returnsREG_ERROR = 0xFE DEV_ERR_PARAM, with the mirror left at its prior value. - Cold install timing: ~2.1 s on the UI3 (3 × ~700 ms flash erase).
- Warm boot timing: ~70 ms (read-compare-write skips all flash erase).
- Latency measurement: ~1.4 s from
setup()start to first publish (bench).
4 · Fleet GC sync (billing-grade synchrony)
Goal: to show that fleet sync 3/3 is not academic but a real billing-grade requirement for multi-tenant deployments (sub-metering apartments in a shared panel) and for precise sub-metering (UI1 mains must match the sum of the sub-meter powers to within the duration of a single period).
# Fleet config — all modules in one group for synchronized latching:
rbamp:
- id: tenant1_mains
address: 0x50
sensor_class: SCT_013
ct_model: SCT_013_030
fleet_gc_enable: true
group_id: 1
- id: tenant1_panel
address: 0x51
sensor_class: SCT_013
ct_models: [SCT_013_005, SCT_013_030, SCT_013_020]
fleet_gc_enable: true
group_id: 1
- id: tenant2_mains # Independent tenant (separate group)
address: 0x52
sensor_class: SCT_013
ct_model: SCT_013_030
fleet_gc_enable: true
group_id: 2 # ⚠ different group → independent sync timeline
# Native API services for manual sync verification:
api:
port: 6053
services:
- service: emit_gc_tenant1
then:
- lambda: 'id(tenant1_mains).transmit_gc_frame(1, 0);' # group=1
- service: emit_gc_tenant2
then:
- lambda: 'id(tenant2_mains).transmit_gc_frame(2, 0);' # group=2
- service: check_sync_all
then:
- lambda: |-
uint16_t t1m = id(tenant1_mains).read_gc_tick_received();
uint16_t t1p = id(tenant1_panel).read_gc_tick_received();
uint16_t t2m = id(tenant2_mains).read_gc_tick_received();
ESP_LOGI("rbamp", "GC tick state: tenant1[mains=%u, panel=%u] tenant2[mains=%u]",
t1m, t1p, t2m);
text_sensor:
- platform: template
name: "Tenant1 GC tick (mains)"
update_interval: 30s
lambda: 'return std::to_string(id(tenant1_mains).read_gc_tick_received());'
- platform: template
name: "Tenant1 GC tick (panel)"
update_interval: 30s
lambda: 'return std::to_string(id(tenant1_panel).read_gc_tick_received());'Bench output (HW-validated, 10-iteration sync test)
[I][rbamp:572]: GC frame emit: group=1, tick=42, all 2 tenant1 modules ACK'd
[I][rbamp:580]: check_sync: tenant1_mains.tick=42 tenant1_panel.tick=42 → 2/2 in_sync
[I][rbamp:572]: GC frame emit: group=2, tick=42, tenant2 module ACK'd
[I][rbamp:580]: check_sync: tenant2_mains.tick=42 → 1/1 in_sync (tenant2 isolated)
ROUND 0: GC tick=1 → tenant1[1, 1] tenant2[1] → 3/3 in_sync
ROUND 1: GC tick=2 → tenant1[2, 2] tenant2[2] → 3/3 in_sync
ROUND 2: GC tick=3 → tenant1[3, 3] tenant2[3] → 3/3 in_sync
...
ROUND 9: GC tick=10 → tenant1[10,10] tenant2[10] → 3/3 in_sync
L8 soak: 180 cycles, 0 sync drops, 0 identity dropsHow group_id helps in multi-tenant
Without group_id there is no separation of tenants — all modules react to any
GC frame. With group_id:
- Tenant 1's modules respond only to GC frames with
group=1(or the all-callgroup=0) - Tenant 2's modules respond only to GC frames with
group=2
This lets the master emit separate GC frames for the tenants at independent moments — for example, by billing cycle:
automation:
- alias: "Tenant 1 billing cycle latch"
trigger:
platform: time_pattern
seconds: "/60" # every 60 s
action:
- service: esphome.<node>_emit_gc_tenant1
- alias: "Tenant 2 billing cycle latch"
trigger:
platform: time_pattern
seconds: "/15" # every 15 s (high-resolution tenant)
action:
- service: esphome.<node>_emit_gc_tenant2Validation in production
-
HA template sensor "mains_minus_subsum" — a discrepancy indicator: ```yaml sensor:
- platform: template name: "Mains - SubSum (W)" update_interval: 60s unit_of_measurement: "W" lambda: |- // here the sub-meters are current-only, so this is a rough estimate: // mains_power - sum(sub_currents × V_mains) float v_mains = id(mains_voltage_state).state; float sub_i = id(sub_i0).state + id(sub_i1).state + id(sub_i2).state; return id(mains_power_state).state - sub_i * v_mains; ``` A discrepancy < 1% of mains_power on the bench = sync correct.
-
L9 master-clock canon — even with GC sync 3/3, wall-clock integration guarantees billing-grade Wh accuracy (validated 0.0000% rel_err).
5 · UI1 — single-channel (minimal)
Source: ../example/ui1.yaml
The simplest useful configuration: one voltage channel, one current channel, and six derived quantities from them. A starting point for any new deployment — extend it with more fields as needed.
esphome:
name: rbamp-ui1
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "rbAmp UI1 Fallback"
password: !secret ap_password
captive_portal:
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz # 100 kHz causes periodic NACKs from the module;
# 50 kHz reduces them ~5-10× — see the chapter end link.
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
rbamp:
id: meter1
address: 0x50
update_interval: 60s
ct_model: SCT_013_030 # CT clamp model — change it to match yours
sensor:
- platform: rbamp
rbamp_id: meter1
voltage:
name: "Mains Voltage"
current:
name: "Mains Current"
power:
name: "Mains Power"
energy:
name: "Mains Energy"
frequency:
name: "Mains Frequency"
power_factor:
name: "Mains Power Factor"What it demonstrates: the minimal viable configuration. The six sensors cover
the core case — whole-home metering or a single load. The energy sensor already
has device_class: energy + state_class: total_increasing set, so it goes
straight into the HA Energy dashboard with no extra setup. The period-metering
protocol (latch / 50 ms settle / read average power, integrated on the master
clock) is documented in 09 · API reference.
6 · UI1 with customization — all the standard ESPHome knobs
rbAmp is a "good citizen" ESPHome component. Each slot (voltage, current,
power, …) is an ordinary ESPHome sensor: object. The whole standard set of
properties and filters works on it. You need neither a lambda: nor custom
templates — everything you do with pzemac / atm90e32 / cse7766 works here
exactly the same way. The example shows the most common customizations in a
single block:
esphome:
name: rbamp-ui1-tuned
esp32:
board: esp32dev
framework:
type: arduino
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
rbamp:
id: meter1
address: 0x50
update_interval: 10s # ↑ Poll once / 10 s instead of the default 60 s —
# a denser graph, ~6× the I²C traffic.
ct_model: SCT_013_030
sensor:
- platform: rbamp
rbamp_id: meter1
# ──────────── Customizing names + precision + filters ────────────
voltage:
name: "Mains voltage" # Any name — it becomes the HA entity_id
accuracy_decimals: 1 # 226.7 instead of the default 226.70
filters:
# Linear calibration against a reference multimeter:
- calibrate_linear:
- 0.0 -> 0.0
- 230.0 -> 230.5
# Smoothing of sharp spikes (standard ESPHome):
- exponential_moving_average:
alpha: 0.3
send_every: 1
current:
name: "Load current"
accuracy_decimals: 3 # 0.755 instead of 0.75
filters:
# Mask the noise floor — drop values below 50 mA (50 mW at 230 V).
# The equivalent of the `noise_floor` option in pzemac / cse7766.
- lambda: 'return x < 0.05 ? 0.0f : x;'
power:
name: "Power"
accuracy_decimals: 1
filters:
# Throttle publications: no more than once / 30 s, even with update_interval 10 s.
- throttle: 30s
energy:
name: "Energy (Wh)"
# The standard device_class: energy + state_class: total_increasing
# are already set by the component — the Energy dashboard works.
filters:
# Convert Wh → kWh for a more familiar scale in Lovelace.
# (The HA Energy dashboard itself expects Wh, but if you want a graph in kWh,
# declare a second template sensor with a division rather than changing units here.)
- multiply: 0.001
unit_of_measurement: "kWh" # ← overrides the default "Wh"
accuracy_decimals: 3
frequency:
name: "Mains frequency"
accuracy_decimals: 2
power_factor:
name: "Power factor"
accuracy_decimals: 3
# Optional: a filter to hide PF at zero load (P ~ 0)
filters:
- lambda: 'return isnan(x) ? 0.0f : x;'What it demonstrates:
- Custom names (
name:) become the HAentity_id— for example,sensor.rbamp_ui1_tuned_mains_voltage. Names in any language, including Cyrillic, work. - Output precision (
accuracy_decimals:) is independent of the module; the module returns float32 at maximum precision, and ESPHome truncates it for the UI. update_interval:does not need to be changed per sensor — it's a property of the component (PollingComponent), set once in therbamp:block. If you want rarer publications of individual sensors, addfilters: - throttle: <interval>.filters:— the full ESPHome set:calibrate_linear— point calibration against your reference (multimeter, reference ammeter).exponential_moving_average/sliding_window_moving_average— smoothing.lambda— an arbitrary transform (noise-floor mask, conditional zero).multiply/offset— arithmetic (for example, Wh → kWh).throttle/delta— reducing the publication rate.unit_of_measurement:can be overridden (for example, tokWhwithmultiply: 0.001). HA picks up the new unit automatically.- No
lambda:for basic operation — all the RMS, power, frequency, and PF calculations are done by the module. The ESP32 only renames, formats, and filters.
⚠ Do not override
device_class:forenergy— the HA Energy dashboard strictly expectsdevice_class: energy+state_class: total_increasing. Unit conversion viamultiply+unit_of_measurement:keeps compatibility, but changing thedevice_classwill break the dashboard.
The full list of standard sensor properties and filters is in the
official ESPHome Sensor Component documentation.
That documentation is fully applicable to rbAmp slots — there are no
deviations from the standard.
7 · UI3 — three-channel with mixed clamps
Source: ../example/ui3.yaml
The UI3 SKU has one voltage input and three independent current channels. The canonical scenario is to put different clamp models on different channels: a small clamp on low-current consumers (for maximum resolution) and a large one on the main feed (for headroom on peaks).
esphome:
name: rbamp-ui3
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
rbamp:
id: meter1
address: 0x50
update_interval: 60s
# drdy_pin: GPIO15
ct_models: [SCT_013_005, SCT_013_030, SCT_013_020]
# CH0=5A for standby consumers and LED lighting,
# CH1=30A for the main household loads,
# CH2=20A for the boiler / a medium-power branch (~3-5 kW)
# NOTE v1.3: SCT_013_100 removed (reserved); for > 50A — multiple channels
# with SCT_013_050 in parallel or the roadmap WIRED_CT SKU.
sensor:
- platform: rbamp
rbamp_id: meter1
# Common (topology-independent)
voltage:
name: "Mains Voltage"
frequency:
name: "Mains Frequency"
apparent_power:
name: "CH0 Apparent Power"
# Channel 0 — primary current input (small consumers, 5A)
current:
name: "CH0 Current"
power:
name: "CH0 Power"
energy:
name: "CH0 Energy"
power_factor:
name: "CH0 Power Factor"
reactive_power:
name: "CH0 Reactive Power"
# Channel 1 — main loads (30A)
current_1:
name: "CH1 Current"
power_1:
name: "CH1 Power"
energy_1:
name: "CH1 Energy"
power_factor_1:
name: "CH1 Power Factor"
reactive_power_1:
name: "CH1 Reactive Power"
# Channel 2 — boiler / medium-power branch (20A)
current_2:
name: "CH2 Current"
power_2:
name: "CH2 Power"
energy_2:
name: "CH2 Energy"
power_factor_2:
name: "CH2 Power Factor"
reactive_power_2:
name: "CH2 Reactive Power"What it demonstrates: all three channels of the UI3 SKU + mixed clamp models
via ct_models:. The schema validator checks that power_N requires current_N
and voltage — if you declare power_1 without current_1, compilation fails
with a clear message. Each channel has its own independent NVS-persisted
accumulator energy_*. If all channels use the same clamp model, use the global
ct_model: instead of ct_models:.
8 · Multi-module bus (three rbAmp at 0x50 / 0x51 / 0x52)
Source: ../example/multi_module.yaml
Three modules on one I²C bus — the house's main feed, the solar generation
output, and EV charging. Each has its own rbamp: block (in the component schema,
MULTI_CONF = True).
esphome:
name: rbamp-multi
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: INFO
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz # mandatory: at 100 kHz with three modules, periodic
# NACKs are almost inevitable on every cycle
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
# Three independent rbamp: blocks — MULTI_CONF = True in the schema.
# On v1.3 firmware the general-call latch is opt-in via fleet_gc_enable +
# group_id (capability-gated via CAP_GC_LATCH). The legacy broadcast_latch:
# key is still accepted as a deprecated alias — modules in the same group
# latch in a single transaction, with ~540 µs skew per module. On legacy
# firmware (v1.0..v1.2) the component falls back to a sequential latch
# and logs an informational line.
rbamp:
- id: meter_house
address: 0x50
update_interval: 60s
broadcast_latch: true
- id: meter_solar
address: 0x51
update_interval: 60s
broadcast_latch: true
bidirectional: true # the STANDARD / PRO tiers; energy_exported_*
# publishes 0 until the export accumulator
# appears in the firmware
- id: meter_evcharger
address: 0x52
update_interval: 60s
broadcast_latch: true
sensor:
- platform: rbamp
rbamp_id: meter_house
voltage:
name: "House Voltage"
current:
name: "House Current"
power:
name: "House Power"
energy:
name: "House Energy"
frequency:
name: "Mains Frequency"
- platform: rbamp
rbamp_id: meter_solar
voltage:
name: "Solar Voltage"
internal: true # hidden from HA — the same mains as House Voltage
current:
name: "Solar Current"
power:
name: "Solar Power"
energy:
name: "Solar Energy"
energy_exported:
name: "Solar Energy Exported"
- platform: rbamp
rbamp_id: meter_evcharger
voltage:
name: "EV Charger Voltage"
internal: true
current:
name: "EV Charger Current"
power:
name: "EV Charger Power"
energy:
name: "EV Charger Energy"What it demonstrates: the MULTI_CONF = True pattern — declare as many
rbamp: blocks as you have modules on the bus. Each maintains its own NVS
accumulator, whose key is derived from the I²C address, so the three counters stay
independent across reboots. The internal: true flag on the duplicate voltage
sensors hides the three nearly identical mains-voltage entities from HA when all
modules measure the same phase. For the address-provisioning procedure, see
Example 2 below.
Semantics of
internal: true. The flag does not disable the register read from the module — the component still reads the voltage from each module on everyupdate_interval(this is required for the power calculation). The flag only hides the sensor from the Home Assistant API: no entity is created, it does not appear in Lovelace, and it is not counted in the Energy dashboard. If you want to reduce I²C traffic, do not declare thevoltage:slot undersensor.platform: rbampat all — the component then does not query the register. If you do declare it, theinternal:flag is purely cosmetic.
A second constraint concerns address uniqueness on the bus:
⚠ Unique addresses are the configuration's responsibility. The component schema (
MULTI_CONF = True) does not validate overlappingaddress:values betweenrbamp:blocks. If two blocks specify the sameaddress: 0x50, both declarations are accepted, and on the bus both targets will respond at once — expect I²C conflicts, garbled responses, or symptoms resembling a missing module. Before committing the YAML, verify that alladdress:values are unique (and do not collide with other I²C devices on the bus). After eachnew_address:provisioning, update therbamp:section to the new address — the old entry is not invalidated automatically.
Pull-ups on a multi-module bus
All three modules ship with factory 4.7 kΩ pull-ups. On a bus of three modules, the parallel combination is ~1.6 kΩ — higher current draw and marginal signal integrity at 50 kHz. Cut the pull-up solder jumpers on modules #2 and #3 (leave them only on #1, or place a single external 4.7 kΩ pair next to the ESP32). For more, see 04_hardware.md, the section on the multi-module bus.
9 · One-time I²C address change (0x50 → 0x51)
Source: ../example/address_change.yaml
All rbAmp modules ship from the factory at address 0x50. To put several modules
on one bus, you need to reflash each to its own unique address before
connecting them in parallel.
esphome:
name: rbamp-addr-change
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: DEBUG # DEBUG shows the address-change lines
api:
ota:
- platform: esphome
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
rbamp:
id: meter1
address: 0x50 # current address — before the change
new_address: 0x51 # one-time target — DELETE this line after success
sensor:
- platform: rbamp
rbamp_id: meter1
voltage:
name: "rbAmp Voltage (post-relocate)"
current:
name: "rbAmp Current (post-relocate)"What it demonstrates: the one-time address-change flow. Flashing this YAML makes the component run the procedure once at boot:
- Probe
0x50. If0x50is silent and0x51already responds, the change was applied in a previous boot; the component adapts and logs a warning asking you to clean up the YAML. - Check that the module is ready to accept a provisioning operation.
- Write the new address to flash (~700 ms), a soft reset (~300 ms), and re-lock to the AC cycle.
- Confirm at the new address. On success — the log
Address change confirmed at 0x51andIMPORTANT: update YAML to address: 0x51 and remove new_address:.
Requirement: the module must be in factory-provisioning mode — a mode for one-time provisioning operations. Standard production modules ship with provisioning mode disabled; to change the address in the field, consult the module documentation or the supplier for the procedure to temporarily enable it.
After success: edit the YAML to address: 0x51 and remove the line
new_address: 0x51. Flash again. From the next boot, the component uses 0x51
normally and the change procedure no longer runs.
Revert checklist after a successful provisioning
So as not to leave a "dangling" provisioning variant in the repository:
# BEFORE (provisioning):
rbamp:
id: meter1
address: 0x50 # the module's current address
new_address: 0x51 # the provisioning target
update_interval: 60s
# AFTER (production — applied next flash):
rbamp:
id: meter1
address: 0x51 # ← replaced with the new address
# new_address: 0x51 # ← DELETE the line (do not comment it out)
update_interval: 60sExactly three edits:
- Change the
address:value from the old one to the new one. - Delete (do not comment out) the
new_address:line. - Reflash via OTA or USB.
After step 3, the log should show a normal dump_config line without the
warning address change requested but current address matches new_address —
which means the revert was correct. See also
10_troubleshooting §4 "OTA right after new_address: provisioning won't load"
if the first OTA after the revert doesn't start.
10 · Bench config with secrets
Source: ../example/bench-ui1.yaml
The bench-*.yaml pattern is for development machines where Wi-Fi credentials are
needed in the config but must not end up in a commit. example/.gitignore
excludes bench-*.yaml and secrets.yaml from version control.
esphome:
name: rbamp-ui1
esp32:
board: esp32dev
framework:
type: arduino
logger:
level: DEBUG
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "rbAmp UI1 Fallback"
password: !secret ap_password
captive_portal:
api:
ota:
- platform: esphome
web_server:
port: 80
version: 3 # optional: a local HTTP UI — handy for bench testing
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
scan: true
external_components:
- source:
type: local
path: ../components
components: [rbamp]
rbamp:
id: meter1
address: 0x50
update_interval: 60s
ct_model: SCT_013_030
sensor:
- platform: rbamp
rbamp_id: meter1
voltage:
name: "Mains Voltage"
current:
name: "Mains Current"
power:
name: "Mains Power"
energy:
name: "Mains Energy"
frequency:
name: "Mains Frequency"
power_factor:
name: "Mains Power Factor"The corresponding secrets.yaml (in example/, not tracked):
wifi_ssid: "YourNetworkName"
wifi_password: "YourPassword"
ap_password: "rbamprbamp"What it demonstrates: separating compile-time credentials from
version-controlled YAML via ESPHome's !secret. Listing bench-*.yaml in
.gitignore lets you keep as many bench variants as you like with no leak risk.
Use esphome run --device 192.168.0.173 example/bench-ui1.yaml for OTA after the
first network connection, or esphome run --device COM6 example/bench-ui1.yaml
for the first USB flash.
11 · Brownout and loss of mains
When mains power is interrupted, the module's isolated analog front-end stops
receiving a signal. The voltage register drops to 0.0 V — a valid IEEE 754
finite float that passes the component's isfinite() sanity filter. The
component's filter policy is intentional:
NO physical lower bounds — brownout (U=0 V on mains disconnect), voltage sag, off-grid / UPS test, and breaker-trip MUST pass through to HA, so the user sees the real state.
No special YAML is needed to detect a loss of mains — the voltage sensor itself
reads 0.0 V.
sensor:
- platform: rbamp
rbamp_id: meter1
voltage:
name: "Mains Voltage" # reads 0.0 V when mains is disconnected
current:
name: "Mains Current"
power:
name: "Mains Power"
energy:
name: "Mains Energy"HA automation: alert on loss of mains
# configuration.yaml or the Automations UI
automation:
- alias: "rbAmp mains loss alert"
trigger:
- platform: numeric_state
entity_id: sensor.rbamp_ui1_mains_voltage
below: 10 # U < 10 V = mains disconnected or a severe brownout
for:
seconds: 5 # debounce: sustained, not transient
action:
- service: notify.mobile_app_your_phone
data:
title: "Mains power lost"
message: >
Voltage dropped to {{ states('sensor.rbamp_ui1_mains_voltage') }} V
at {{ now().strftime('%H:%M:%S') }}.What it demonstrates: using the 0 V passthrough behavior as the
source of a "loss of mains" event. Because the component publishes 0.0 V rather
than skipping the publish, HA automations and the Energy dashboard see the
brownout event in real time. The for: 5 seconds condition blocks false
triggers during the ~250 ms boot warm-up (when the registers read 0 before the
first valid measurement).
12 · HA Energy dashboard
The energy sensor (and energy_1, energy_2, energy_exported, etc.) is
automatically configured with device_class: energy and
state_class: total_increasing — exactly what the HA Energy dashboard expects for
cumulative consumption sensors. No extra YAML is needed.
After discovery:
- Settings → Dashboards → Energy.
- Add Consumption → choose "Mains Energy" (or the import-metering entity you want).
- If you have solar — Add Production → choose Solar Energy (from
meter_solarin the multi-module example).
Energy values are in Wh. HA converts them to kWh for display itself. The NVS persistence in the component ensures the accumulator survives an ESP32 reboot without a false "counter reset" in the HA statistics.
# Example: consumption + solar export for the HA Energy dashboard
sensor:
- platform: rbamp
rbamp_id: meter_house
energy:
name: "House Grid Import" # → HA Energy: grid consumption
- platform: rbamp
rbamp_id: meter_solar
energy:
name: "Solar Production" # → HA Energy: solar production
energy_exported:
name: "Solar Grid Export" # → HA Energy: return to grid
# reads 0 until the export accumulator
# for STANDARD/PRO appears in the firmware13 · HA automations on rbAmp sensors
Load shedding: a relay turns off when a threshold is exceeded
automation:
- alias: "Load shedding — turn off non-critical loads at 3 kW"
trigger:
- platform: numeric_state
entity_id: sensor.rbamp_multi_house_power
above: 3000 # 3 kW threshold
for:
seconds: 10
action:
- service: switch.turn_off
target:
entity_id: switch.ev_charger_relay # the non-critical load relay
- alias: "Load shedding — restore when it drops below 2 kW"
trigger:
- platform: numeric_state
entity_id: sensor.rbamp_multi_house_power
below: 2000
for:
seconds: 30
action:
- service: switch.turn_on
target:
entity_id: switch.ev_charger_relayPower factor correction reminder
automation:
- alias: "Poor power factor notification"
trigger:
- platform: numeric_state
entity_id: sensor.rbamp_ui1_mains_power_factor
below: 0.7
for:
minutes: 5
action:
- service: persistent_notification.create
data:
title: "Low power factor"
message: >
Power factor is {{ states('sensor.rbamp_ui1_mains_power_factor') }}.
Check for large inductive loads (motors, old lighting ballasts).Daily budget overrun alert
automation:
- alias: "Daily energy budget — warning at 10 kWh"
trigger:
- platform: template
value_template: >
{{ states('sensor.energy_daily') | float(0) > 10000 }}
action:
- service: notify.mobile_app_your_phone
data:
message: "Daily energy usage exceeded 10 kWh."14 · Lovelace cards
Gauge — live power
type: gauge
entity: sensor.rbamp_ui1_mains_power
min: 0
max: 3600
name: "Live Power"
segments:
- from: 0
color: green
- from: 1500
color: orange
- from: 2500
color: redApex Charts — power over time
type: custom:apexcharts-card
header:
title: "Power (W)"
show: true
graph_span: 4h
series:
- entity: sensor.rbamp_ui1_mains_power
name: Power
stroke_width: 2
curve: smoothApex Charts — energy comparison across three channels
type: custom:apexcharts-card
header:
title: "Channel Energy (Wh)"
show: true
graph_span: 24h
series:
- entity: sensor.rbamp_ui3_ch0_energy
name: CH0
- entity: sensor.rbamp_ui3_ch1_energy
name: CH1
- entity: sensor.rbamp_ui3_ch2_energy
name: CH2Mini graph — voltage
type: custom:mini-graph-card
entities:
- sensor.rbamp_ui1_mains_voltage
name: "Mains Voltage"
hours_to_show: 12
points_per_hour: 4
line_width: 215 · Utility meter — aggregation by day / week / month
ESPHome publishes cumulative totals in Wh. The HA utility_meter integration
resets the counter at configurable intervals and publishes the delta as a separate
sensor — handy for daily or monthly billing breakdowns.
# configuration.yaml
utility_meter:
energy_daily:
source: sensor.rbamp_ui1_mains_energy
cycle: daily
name: "Daily Energy"
energy_monthly:
source: sensor.rbamp_ui1_mains_energy
cycle: monthly
name: "Monthly Energy"
energy_weekly:
source: sensor.rbamp_ui1_mains_energy
cycle: weekly
name: "Weekly Energy"HA creates three entities: sensor.energy_daily, sensor.energy_monthly,
sensor.energy_weekly. Each resets to 0 at the start of its respective period and
accumulates the delta from the source total_increasing sensor.
Derived kWh/day sensor
utility_meter outputs Wh by default. To display in kWh, add a template
sensor:
template:
- sensor:
- name: "Daily Energy kWh"
unit_of_measurement: "kWh"
state_class: total_increasing
device_class: energy
state: >
{{ (states('sensor.energy_daily') | float(0) / 1000) | round(3) }}Daily cost sensor
template:
- sensor:
- name: "Daily Energy Cost"
unit_of_measurement: "EUR"
icon: mdi:currency-eur
state: >
{% set kwh = states('sensor.energy_daily') | float(0) / 1000 %}
{% set rate = 0.28 %} # EUR/kWh — set your own tariff
{{ (kwh * rate) | round(2) }}16 · Per-load sub-metering (3 modules at 0x50 / 0x51 / 0x52)
Installing three rbAmp modules in a distribution panel lets you do per-circuit energy metering with no smart panel or extra hardware.
# Extending the multi-module example with per-circuit utility meters
utility_meter:
kitchen_daily:
source: sensor.rbamp_multi_house_energy
cycle: daily
name: "Kitchen Circuit Daily"
solar_monthly:
source: sensor.rbamp_multi_solar_energy
cycle: monthly
name: "Solar Production Monthly"
ev_daily:
source: sensor.rbamp_multi_ev_charger_energy
cycle: daily
name: "EV Charging Daily"Combined with the HA Energy dashboard, you get monthly statements per circuit with no extra kWh×tariff calculations.
17 · rbAmp + a relay — closed-loop control
A boiler with relay control
# ESPHome YAML on the same ESP32 as rbAmp
switch:
- platform: gpio
pin: GPIO5
id: heater_relay
name: "Water Heater"With rbAmp and a relay on the same ESP32, you can build a closed loop right in
ESPHome (via a Lambda or on_value) with no round trip through HA:
sensor:
- platform: rbamp
rbamp_id: meter1
power:
name: "House Power"
on_value:
then:
- if:
condition:
lambda: 'return x > 3000.0f;'
then:
- switch.turn_off: heater_relay
else:
- switch.turn_on: heater_relayWhat it demonstrates: using a rbAmp sensor's on_value callback to control a
relay without HA involvement. The callback fires on the ESP32 every
update_interval (60 s) and updates the relay state from the latest power
reading. For tighter control (a < 60 s loop), reduce update_interval to 10s —
this increases the load on the I²C bus proportionally. A more complete closed-loop
example for a boiler is in 07_diy_integrations.md.
18 · DRDY integration
The module's open-drain DRDY pin gives a LOW pulse of ~10 µs every ~200 ms after the instantaneous registers are updated. You can wire it to an ESP32 GPIO and use it as an interrupt hint instead of polling on a fixed timer.
rbamp:
id: meter1
address: 0x50
update_interval: 60s # energy integration still happens every 60 s
drdy_pin:
number: GPIO15
mode:
input: true
pullup: true # DRDY is open-drain; it needs a pull-up
inverted: true # the DRDY pulse is LOW; inverted=true gives a rising edgeNote: on the current firmware the component logs
drdy_pinindump_configbut does not implement interrupt-driven RT reads — theupdate_intervalstill sets the polling rate. You can wire the pin up ahead of time for future firmware revisions with no code changes.
19 · Preparing for future split-phase / three-phase SKUs
The component schema already accepts phased sensor fields (voltage_a/b/c,
current_a/b/c, power_a/b/c, energy_a/b/c, power_factor_a/b/c,
reactive_power_a/b/c, power_total). They are reserved for the upcoming
rbAmp-U2I2 (split-phase US) and rbAmp-U3I3 (three-phase Europe) SKUs.
If you're preparing YAML for a future three-phase deployment today, you can
describe the full phased block and validate it now. On the current firmware it
compiles cleanly (there just won't be real registers to read). The schema
validator checks that single and phased groups are not mixed in one sensor:
block.
# Forward-compat three-phase config — compiles today, sensor data
# will become available when the rbAmp-U3I3 firmware ships. DO NOT USE on
# current single-channel UI* — readings will be 0.
sensor:
- platform: rbamp
rbamp_id: meter1
voltage_a:
name: "Phase A Voltage"
voltage_b:
name: "Phase B Voltage"
voltage_c:
name: "Phase C Voltage"
current_a:
name: "Phase A Current"
current_b:
name: "Phase B Current"
current_c:
name: "Phase C Current"
power_a:
name: "Phase A Power"
power_b:
name: "Phase B Power"
power_c:
name: "Phase C Power"
power_total:
name: "Total Power"
energy_a:
name: "Phase A Energy"
energy_b:
name: "Phase B Energy"
energy_c:
name: "Phase C Energy"
frequency:
name: "Grid Frequency"What it demonstrates: the phased field group. Mixing single and phased fields
in one sensor: block produces a compile-time error — choose the group for your
hardware SKU. The topology field group and SKU variant detection are documented
in 09 · API reference.
20 · Migration from other ESPHome metering components
From pzem004t
The pzem004t platform exports the same field names as rbamp for the common AC
quantities. The swap is block-for-block with identical sensor names:
# Before (pzem004t, UART)
uart:
rx_pin: GPIO16
tx_pin: GPIO17
baud_rate: 9600
sensor:
- platform: pzem004t
voltage:
name: "Mains Voltage"
current:
name: "Mains Current"
power:
name: "Mains Power"
energy:
name: "Mains Energy"
frequency:
name: "Mains Frequency"
power_factor:
name: "Mains Power Factor"
# After (rbamp, I²C) — the same sensor names → the same entity IDs in HA
i2c:
sda: GPIO21
scl: GPIO22
frequency: 50kHz
rbamp:
id: meter1
address: 0x50
ct_model: SCT_013_030
sensor:
- platform: rbamp
rbamp_id: meter1
voltage:
name: "Mains Voltage"
current:
name: "Mains Current"
power:
name: "Mains Power"
energy:
name: "Mains Energy"
frequency:
name: "Mains Frequency"
power_factor:
name: "Mains Power Factor"Identical name: fields give identical entity IDs in HA. The Energy dashboard
history is preserved across the swap: the total_increasing sensor picks up where
it left off. Remove the uart: block if it isn't shared with another component.
From cse7766 (Sonoff POW R2)
Same pattern. cse7766 provides voltage, current, power, energy,
power_factor, apparent_power — all present in rbamp. Replace
platform: cse7766 with platform: rbamp, add rbamp_id: meter1, remove
uart:, and add i2c: and the rbamp: block.
YAML validation rules summary
| Rule | Enforced by |
|---|---|
power / energy / power_factor / reactive_power require current |
Schema validator (_SINGLE_SLOT_DEPS) |
P/Q/PF/E for any channel require voltage |
Schema validator |
Single and phased groups are not mixed in one sensor: |
_validate_topology_consistency |
new_address ≠ address |
_validate_new_address |
address in the range 0x08..0x77 |
cv.i2c_address |
update_interval minimum 1s, enforced |
cv.positive_time_period_milliseconds(min=1000) |
ct_model: and ct_models: are mutually exclusive |
Schema validator |
Validation errors are reported at esphome compile time with a human-readable
message pointing to the offending key. You don't need to flash hardware to find a
configuration error.