Ulrich Lehner

LINZ NETZ Smart Meter in Home Assistant · Part 1

Reading a LINZ NETZ Smart Meter in Home Assistant with an RTL-SDR Dongle

LINZ NETZ smart meters broadcast their readings by radio every five seconds. An RTL-SDR dongle on a Home Assistant Raspberry Pi receives them, with no cloud and no hardware at the meter: live power draw and billing-grade import and export readings for the Energy dashboard.

Illustration of a smart meter on a wall at dusk sending radio waves past a padlock symbol to a small single-board computer with a USB radio dongle and antenna, which passes the data to a laptop showing an energy dashboard with a power gauge, a consumption chart and tiles for home and solar
AI-generated illustration

The meter's customer interface (Kundenschnittstelle) uses wireless M-Bus, mode T1 on 868.95 MHz. The telegrams are AES-128 encrypted with a personal key from the LINZ NETZ service portal. The open-source tool wmbusmeters decodes them, and its Home Assistant app publishes the values via MQTT. This guide walks through the whole chain. It works even if your meter sits outside your flat, as long as the dongle is in radio range. The stock decoder sometimes reports wrong energy values for these meters, so Step 4 installs a small custom driver before the meter is added.

What you need

ItemNotes
Home Assistant on a Raspberry Pi 4 or 5Home Assistant OS, so apps are available; tested on a Pi 5
RTL-SDR USB dongle (R820T/R828D tuner)For example the RTL-SDR Blog V3 or V4
868 MHz antennaThe dipole from the dongle kit (each arm about 8.2 cm) or a ready-made 868 MHz/LoRa whip
USB extension cable, 1 to 2 mOptional. Keeps the dongle clear of USB 3 noise and lets you move it to a better spot
LINZ NETZ service portal accountTo activate the interface and fetch the key

The Pi must be within radio range of the meter cabinet. A meter in the building's meter cabinet, outside the flat, is typically received reliably on auto gain.

Step 1: Activate the customer interface and get the key

The meter only transmits on its customer interface once you activate it.

  1. Log in to the LINZ NETZ service portal and open Online-Services, then Serviceportal, then Meine Kundenschnittstelle, then Kundenschnittstelle aktivieren.
  2. Select your installation (Basisanlage) and activate it.
  3. Wait. LINZ NETZ provisions the meter remotely and provides the access code (Zugangscode) within a few days.
  4. Open the same page again and click through: the code is shown there for 14 days only. Store it right away, for example in a password manager.

The code is the AES key: 32 hex characters, without spaces. Treat it like a password and keep it out of screenshots, forums and Git repositories.

Step 2: Check reception on a laptop (optional)

Before touching Home Assistant, you can check with the same dongle on a laptop whether the meter is reachable from where the Pi will stand. On macOS with Homebrew:

Shell
brew install rtl_433
# enable all wireless M-⁠Bus decoders and listen on 868.95 MHz
rtl_433 -⁠f 868.95M -⁠s 1200k -⁠F json \
  $(rtl_433 -⁠R help 2>&1 | grep -⁠i m-⁠bus | grep -⁠oE '\[[0-⁠9]+\]' | tr -⁠d '[]' | sed 's/^/-⁠R /')

Your meter shows up as "M" : "DEV" with "type" : 2 (electricity), every five seconds, with an encrypted payload. Other wireless M-Bus devices in the building (heat and water meters) will appear too; ignore them. A PLL not locked warning at start is harmless.

To test decryption offline, take one telegram as hex and run it through wmbusmeters (on macOS it builds from source, see its README):

Shell
wmbusmeters -⁠-⁠analyze=<KEY> <TELEGRAM_HEX>

It should report decrypt check bytes (OK) and pick the driver amiplus. No telegrams at all right after activation? The meter stays silent until LINZ NETZ has finished provisioning, which can take a few days.

To skip the manual steps, the helper script wmbus-test.sh in the smartmeter folder of ulrichlehner/homebase wraps all of this for macOS: setup installs and builds the tools locally, scan searches for the meter across frequencies and gain settings, and analyze decrypts the last telegram with your key.

Step 3: Install Mosquitto and the wmbusmeters app

  1. Plug the dongle into a USB 2.0 port of the Pi (on a Pi 5 the black ones, not the blue USB 3.0 ports, whose noise can disturb the radio) and place the antenna upright, as high as practical and away from metal. A USB extension helps if the Pi sits in a bad spot for reception.
  2. Settings, Apps, Install app: install and start Mosquitto broker. Home Assistant then offers the MQTT integration; add it.
  3. On the same Install app page, open the menu (⋮), then Repositories, and add https://github.com/wmbusmeters/wmbusmeters-ha-addon.
  4. Install and start the wmbusmeters app. Without a meter configured it only listens.

The app ships rtl_sdr and rtl_wmbus, so nothing else is needed on the Pi. The setup lives in the app's own web interface (Open Web UI), not in its Configuration tab.

Step 4: Install the custom driver

The stock amiplus driver misreads the energy counters of these meters from time to time (details in Why a custom driver). The custom driver amiplus_linznetz fixes that, so install it before adding the meter. It is one file from the smartmeter folder of ulrichlehner/homebase: drivers/amiplus_linznetz.xmq.

Written for app version 3.1.0 on Home Assistant OS. Leave wmbusmeters config location on the Home tab at its default.

In the web interface, open the Drivers tab and add a driver named amiplus_linznetz.xmq. Keep the extension: the app saves the name as typed, and wmbusmeters only loads .xmq files. Paste the content of drivers/amiplus_linznetz.xmq.

Step 5: Configure the meter and verify

In the app's web interface, add the meter under Meters, Add New meter:

SettingValue
nameA short, unique name such as <Place> grid, see below
driveramiplus_linznetz
idYour eight-digit wireless M-Bus ID
keyThe access code from the service portal

Don't know the meter ID yet? The app from Step 3 is running without a meter, so its log lists every telegram it ignores, and yours is the one with manufacturer DEV. That ID is not the meter number or the metering point number from the portal, so copy it from the log.

Under MQTT, check Enable mqtt discovery. Set device to this command and leave the other parameters (format, loglevel, shell and so on) at their defaults, then click Save Changes:

rtlwmbus:CMD(rtl_sdr -⁠f 868.95M -⁠s 1600000 -⁠ 2>/dev/null | rtl_wmbus -⁠f -⁠p s -⁠a -⁠t 0)

The default device makes rtl_wmbus decode three radio modes (S1, T1 and C1), each with two methods, although the meter only sends T1 on 868.95 MHz. The command limits it to that: -p s skips S1, -a uses a faster arctangent, -t 0 keeps one of the two methods, and -f lets wmbusmeters restart the receiver when the dongle stops delivering data. On a Raspberry Pi 5 this cuts the app's CPU load from 22 % to under 8 % of one core, with a telegram still every five seconds. Setting only the frequency (rtlwmbus:868.95M) saves nothing, because rtl_wmbus decodes S1 unless -p s is given. If telegrams go missing, remove -t 0.

As plain wmbusmeters meter configuration the same settings read:

Config
name=<Place> grid
driver=amiplus_linznetz
id=<METER_ID>
key=<KEY>

The meter's name is more than a label. It becomes the MQTT topic (wmbusmeters/<name>), the device name in Home Assistant and the prefix of all its entity names. A readable name works: Home Assistant shows <Place> grid total energy consumption and turns it into the entity ID sensor.<place>_grid_total_energy_consumption. Keep it unique per meter: two meters with the same name publish to the same topic and overwrite each other. A good pattern is <Place> <Role>, where the place is, for example, the municipality and the role says what the device is: <Place> grid for the grid meter, later perhaps <Place> PV for a solar inverter. Decide before the first start: a new name means a new topic, and Home Assistant keeps the old entity IDs (it restores them through the unique ID even after you delete the device), so you would have to rename them by hand in the entity settings.

Restart the app and check the log: no No such driver, and Started config rtlwmbus listening on none using CMD(...) at the end (none is expected with a custom command). The log prints the key, the ID and the meter name in clear text on every start, so never paste it into a forum or a repository without blanking those lines.

To see the raw JSON, open Settings, Devices & services, MQTT, Configure, and listen to wmbusmeters/#:

JSON
{
  "meter": "amiplus_linznetz",
  "media": "electricity",
  "current_power_consumption_kw": 0.09,
  "current_power_production_kw": 0,
  "total_energy_consumption_kwh": 123.456,
  "total_energy_production_kwh": 4.2,
  "rssi_dbm": 20
}

With discovery enabled, the app builds the device from the driver as soon as the meter's first telegram arrives: sensors for total energy (import and export), current power (import and export), the meter's date and time, and the signal strength. The total energy sensors already carry device_class: energy and state_class: total_increasing.

Step 6: Add the meter to the Energy dashboard

The energy bars and totals of the Energy dashboard are built from the long-term statistics of your sensors, not from their live state. Only the optional live power display (item 6 of the list below) reads the current state. That is why only sensors with the right metadata (device_class: energy, state_class: total_increasing, unit kWh) can be picked, and why the first bars take a while. The sensors from the discovery have it.

  1. Open Settings, Energy. The page has tabs for Electricity, Gas and Water. Stay on the Electricity tab.
  2. In the section Electricity grid, click Add grid connection.
  3. Under Energy imported from grid, pick the total energy consumption sensor of your meter, for example sensor.<place>_grid_total_energy_consumption.
  4. Leave Energy exported to grid empty if you do not feed in. Without solar the export counter of a new meter sits on a fixed starting value and never moves, so it would only add a section that measures nothing. Once you have a PV system, edit the connection and add the total energy production sensor here.
  5. Under Cost tracking, choose how to track costs (none, a fixed price per kWh or a price sensor).
  6. Optional: under Power measurement, Type of power measurement, you can add live power to the dashboard. Without feed-in choose Standard and pick the current power consumption sensor, because a positive value means import. With a PV system choose Two sensors and pick the current power consumption sensor for import and the current power production sensor for export, and fill in both fields. Without a power sensor the dashboard shows the hourly bars only.
  7. Save. You can also put the current power sensor on a dashboard card or use it in automations, for example a notification when the base load stays above a threshold overnight.

Expect a delay. Home Assistant says a new device can take up to two hours to show data; in practice the first bar appears after the next full hour. For a few minutes the page may also warn about "Statistics not defined" for the sensor. That is normal for a fresh entity and goes away once the first statistics are written.

Why a custom driver

With the stock driver the Energy dashboard sometimes shows hourly bars of tens to thousands of kWh. The reason is in the telegram itself.

LINZ NETZ describes its interface in the document LN-666, "Beschreibung Wireless M-Bus Schnittstelle" (linked under Sources). Once decrypted, a telegram is a list of records, each a value with a short header in front. The first byte, the DIF (data information field), says how the value is stored; 0E means twelve decimal digits. The next byte, the VIF (value information field), says what the value is and in which unit. For energy it also carries a power of ten: VIF 03 means Wh, 04 tens of Wh, 05 hundreds of Wh, 07 units of 10,000 Wh, 00 thousandths of a Wh. For export the VIF gets an extension byte, 3C, which marks the reverse direction (03 becomes 83 3C, 07 becomes 87 3C).

LN-666 specifies the energy counters in Wh, as the records 0E 03 (import) and 0E 83 3C (export). Several times a day, for about half an hour each, the meter sends other energy records instead:

RecordImportExportDigitsWhat the stock amiplus driver reports
As in LN-6660E 03 (Wh)0E 83 3CWhCorrect
VIF 04 to 070E 04 to 0E 07 (10 to 10,000 Wh)0E 84 3C to 0E 87 3CSometimes in that unit, sometimes the counter in Wh or in tenths of a Wh with a wrong VIFCorrect, or 10 to 100,000 times too high
All zero0E 000E 80 3CAll zero0 kWh

One telegram does not say which of the two it is: 000000000008 can be 8 Wh with a wrong VIF or 8 times 10,000 Wh with a right one. Telling them apart needs the previous counter value, and a decoder sees one telegram at a time, so no driver reads these records correctly every time. The power values are never affected. A total_increasing sensor counts every drop as a meter reset and the return as consumption of the whole counter, so each phase books kWh that never flowed.

The custom driver amiplus_linznetz from Step 4 therefore only accepts the records LN-666 specifies:

  • The energy values come only from 0E 03 and 0E 83 3C, scaled normally. Any other energy record gives no value (null); power and date still come through. Home Assistant shows unknown for the energy sensors during such a phase and skips it in the statistics. Nothing is lost: the counter after the phase contains everything used in between, so that consumption lands in the hour after the phase. The LINZ NETZ Energy app splits it between the right hours by the power readings and adds the portal history for the Energy dashboard.
  • Its detection data is a dummy, so it is never picked automatically and the stock amiplus keeps working for other meters. You select amiplus_linznetz by name in the meter settings (Step 5).
  • It leaves out the voltage and tariff fields, which LINZ NETZ telegrams don't carry.
field {
    name           = total_energy_consumption
    quantity       = Energy
    vif_scaling    = None
    force_scale    = 0
    null_value     = 0
    match {
        measurement_type = Instantaneous
        vif_range        = AnyEnergyVIF
    }
}
field {
    name           = total_energy_consumption
    quantity       = Energy
    vif_scaling    = Auto
    dif_signedness = Signed
    match {
        difvifkey = 0E03
    }
}

The first definition turns any energy record into no value, the second one, for the LN-666 record, wins when that record is there. So the energy fields are in every telegram, which the app needs: it creates the Home Assistant entities from the first telegram after each start, and without the field a start during such a phase would remove the energy sensors. The export fields are the same with add_combinable = BackwardFlow and difvifkey = 0E833C. Everything else follows the stock amiplus, including the field names, so the app creates the same entities as for the stock driver, only without the fields this meter does not send. The repository also holds synthetic test telegrams, a week of real ones (anonymised, unencrypted) and a script that runs them through the stock and the custom driver, so you can see the difference without a meter.

Things that look wrong but aren't

  • Export counter above zero without solar. A new meter can leave the factory with a few kWh on both registers from calibration testing; the export value then never moves. The Energy dashboard only counts increases, so it doesn't matter. Only worry if it starts climbing.
  • Import counter higher than your consumption since installation. Same reason: test energy from calibration before the meter was installed.
  • rssi_dbm of +20 dBm. With an RTL-SDR this is an uncalibrated relative level, not real dBm. Higher is better, but it jumps with auto gain. The reliable signal is the update rate: one telegram every five seconds means reception is fine.
  • The energy sensors show unknown for about half an hour, several times a day. That is the custom driver ignoring an energy record that does not match LN-666, see Why a custom driver. The power sensors keep updating, and when the meter returns to the specified record, the counter continues with the current value, which includes everything used in between.

Sources