Local communication between Home Assistant and NIBE heat pumps and indoor modules. No cloud, no myUplink account.
The S-series has Modbus TCP built in, so it needs no extra hardware. The F-series, which has no Modbus TCP, is reached through a NibeGW gateway on the pump's RS-485 bus, built with Nibe F-series ModbusAdapter.
It differs from the existing options in two ways, both of which came out of actually probing a pump rather than reading the documentation:
- It finds out which registers your pump really has. NIBE publishes one register map per model, but any individual unit implements a subset — and answers a superset of the published map. The integration probes the pump at setup and creates entities only for registers that exist, marking the ones that exist but report no value.
- It explains every register in plain language. NIBE's own titles are
service-manual shorthand; roughly a third of them are nothing but a component
designation. Register 31624 on the pump this was built against is titled
exactly
(EB100-EP15-BT28). Here it becomes "Utetemperatur (givare i kompressormodulen)" with a description attached to the entity.
Home Assistant already ships nibe_heatpump,
which supports S-series over Modbus and works. If it covers what you need, use it.
This integration is for the cases it does not cover: registers missing from the model dropdown, entities you cannot identify, and the timeouts that come from reading registers the pump does not implement.
Three behaviours of the S-series Modbus stack shape the whole design:
| Behaviour | Consequence |
|---|---|
| Unimplemented registers answer exception 01 (illegal function), where the spec says 02 (illegal data address) | 01 has to be read as "no such register here", not "this device is broken" |
| A block read succeeds only if every register in the span exists — one hole fails the whole request | Reads must be grouped into contiguous runs of known-live registers |
The firmware answers for hardware that is not fitted, returning a per-datatype sentinel (0x8000, 0xFFFF, -128, 0xFF) |
"Does the register exist" and "is it reporting" are different questions, and both matter |
That last one is why model detection by register fingerprint does not work. On the development unit it confidently reported an exhaust-air S735 for what the readings plainly showed to be a ground-source pump: brine circuit live at 5.8 °C in / 2.1 °C out, every fan at 0 rpm, no exhaust-air sensors at all. The S735 map simply happens to be the most complete one published. So the model is now just a label you pick, and register maps from every S-series model are merged into one union (only 8 of 1611 shared registers carry a conflicting definition).
HACS → Custom repositories → add https://github.com/beolink/HA-Nibe-Easyconf
as an Integration → install → restart Home Assistant.
Manually: copy custom_components/nibe_local_easyconf/ into your
config/custom_components/ directory and restart.
This section is the S-series. For the F1145 up to the VVM 500, see F-series through a NibeGW gateway.
On the pump: enable network (menu 5.2) and Modbus TCP (menu 7.5.9).
You should not need to know the pump's IP address. There are three ways in:
It finds you. NIBE names its pumps NIBE-<serial> on the network, so the
integration matches that hostname over DHCP and Home Assistant offers the pump
as a discovered device on its own. The serial is used as the identity, so the
pump is still recognised after its DHCP lease hands it a different address.
Matching on MAC address was considered and dropped. NIBE holds no IEEE OUI — the pump's MAC belongs to whichever network chipset is fitted, so an OUI rule would match unrelated hardware.
You search. A DHCP lease may not renew for hours, so Settings → Devices & services → Add integration → Nibe Local Easyconf → "Search the network" sweeps Home Assistant's own subnet. Port 502 alone proves nothing — inverters and PLCs sit there too — so every hit is confirmed by reading the outdoor-temperature register, and the reading is shown next to the address. A /24 takes a couple of seconds.
You type it in. Always available, and offered automatically if the search comes up empty.
Whichever route, it then scans the registers — about a minute, once — and shows what it found before asking which model to name the device after.
The scan result is cached, so restarts are instant. It is read again on its own after an update of this integration, because what is worth reading is the integration's opinion as much as the pump's: a release that starts reading registers it did not read before would otherwise only show them on installations set up after it. If the pump happens to be quiet at that moment the scan from before is kept, and the next start tries again.
After a firmware update or fitting an accessory, call the
nibe_local_easyconf.rescan_registers service.
The F-series - F1145/F1245, F1155/F1255, F1345, F1355, F370/F470, F730, F750, SMO 20/40 and VVM - has no Modbus TCP. It is reached through a NibeGW gateway on the pump's RS-485 accessory bus instead: a small ESP32 that stands in for NIBE's MODBUS 40 accessory, acknowledges the pump's telegrams on its own, and relays reads and writes over UDP. This path was built and measured on an F1255-16 through a Waveshare ESP32-S3-RS485-CAN running esphome-nibe.
An SMO or a VVM houses no compressor of its own: the heat pump stands
outside, and NIBE puts the whole of it under the designation EB101. Its
compressor frequency and state, its starts, its own outdoor sensor, the
defrosting, the fan and the output it calculates are read by default on those
models, so the machine making the heat is not the one part of the installation
the page leaves out. Taken from NIBE's register maps rather than from a pump on
the bench: the measurements here were made on an F1255.
The gateway is a project of its own, Nibe F-series ModbusAdapter: ESPHome firmware for a Waveshare ESP32-S3-RS485-CAN, with how to build it, wire it to the pump's input board and switch MODBUS 40 on in the pump. This integration was built and measured against it.
Name the gateway nibe-<the pump's serial>-gw, as that project's example does.
The integration reads the model and the build date from the serial just as it
does for the S-series, and the suffix keeps the gateway apart from the pump's
own network name.
The gateway is offered as a discovered device, through DHCP or through the mDNS record every ESPHome device publishes. Otherwise: Add integration → Nibe Local Easyconf → F-series through a NibeGW gateway, with the gateway's address. Home Assistant's own address has to be among the gateway's allowed sources, or it drops the requests and says so in its log.
There is no model to pick: the pump announces itself every fifteen seconds as, for instance, F1255-16 CU with its software version. Setup then reads the registers worth showing by default - 43 on an F1255, in under a minute - and switches on those that report a value.
Measured on the F1255-16 CU:
| S-series, Modbus TCP | F-series, NibeGW | |
|---|---|---|
| A read | a block of up to 125 registers, ~40 ms | one register, ~1.1 s |
| A register the pump does not implement | an exception | answers anyway |
| Hardware that is not fitted | NIBE's "no reading" value | the same, in 71 of 982 registers |
| The model | picked, or from the serial | the pump's own product message |
Since every register answers, which ones exist cannot be probed: 901 of the 982 registers in the F1155/F1255 map returned a value, including an exhaust air sensor on a pump that has none. So on the F-series:
- Setup reads only the default set. The rest of the map still becomes entities, switched off, and a scan of all of it would take a quarter of an hour.
- Polling reads what is due, not everything. Measurements every cycle, counters every five and settings every ten, most overdue first; a register that fails backs off, doubling up to an hour. A cycle may spend three quarters of the update interval reading, so the default minute fits about 40 reads; what does not fit is read first in the next cycle.
- Registers in LOG.SET are never read. Up to 20 registers loaded into the pump from a USB stick arrive on their own every two seconds. A ready file for the F1155/F1255, a generator for other models and how to load them are in Nibe F-series ModbusAdapter.
The device page shows the product message's model and software version. The F-series keeps no electricity counter, so its coefficient of performance is built rather than read; see counting what the F-series does not measure.
Not something the pump will tell you over Modbus. Every one of the 919 registers
the development unit implements was checked for its serial number — 16-bit and
32-bit, both word orders — and none carries it. A Heat pump type register
exists in every S-series map, but no published map says what its values mean.
Home Assistant's own nibe_heatpump resolves this by asking you in a dropdown.
The pump does announce its serial, though: it calls itself NIBE-<serial> on
the network. That reaches Home Assistant through DHCP, and a reverse DNS lookup
recovers it for pumps added by hand. NIBE
documents the format:
| Digits | Meaning | Development unit |
|---|---|---|
| 1–6 | Article number: model, size and variant | 065443 — S1155-16 |
| 7–8 | Year of manufacture | 22 — 2022 |
| 9–11 | Day of that year (not a week number) | 034 — 3 February |
| 12–14 | Internal sequence number | kept on the device only |
The article number pre-selects the model in setup, and the device page shows the exact model with its size, the article number, the serial, the control board's software version, and a Manufactured date. The dates check out against the machine: built 3 February 2022, 25,025 compressor hours at 4.6 years old is a 62 % average duty cycle, which is what a ground-source pump in Sweden does.
The article table covers what could be corroborated. Retailer listings turned
out to mix NIBE's real six-digit 06xxxx numbers with their own SKUs and
Swedish RSK numbers, so only the former were kept. An unlisted article still
decodes its date, and the anonymous report carries it, so the table can grow
from the fleet.
Every register the pump implements becomes an entity, but only about a hundred are enabled by default — the temperatures, compressor, power, energy, alarms, degree minutes, heat curve and hot water settings a normal installation cares about. The rest are created disabled; enable any of them from the entity settings and it joins the next poll cycle.
Only registers backing an enabled entity are polled, so the cost of the long tail is zero until you ask for it.
Each entity carries three attributes:
description— the generated explanationmodbus_register— the NIBE register numbernibe_title— NIBE's original title, unmodified
NIBE's titles run to 78 characters ("Energy log - Used energy by additional heater for hot water over the past hour"), and the device page cuts a name off at around 25. The default set therefore gets hand-written short names of at most 24 characters — that one becomes Elpatron, VV, 1 h. The full title and the explanation stay on the entity as attributes, one click away.
NIBE also reuses titles: on the development unit 308 of the 919 registers shared just 57 names, 21 of them titled only "Permit". Every name is made distinct — by the component designation that tells them apart where there is one (EP14 and EP15 are the two refrigerant circuits), by marking the setting when a value exists both to read and to set, and by the register number when nothing else is known.
The explanation is in the entity's details rather than a hover tooltip, because
the device page has nowhere to put one: the card that draws its entity rows
binds no title and uses no tooltip component, and the only place the frontend
renders a description attribute at all is the legacy configurator dialog.
Checked against home-assistant-frontend==20260826.6, the version Home
Assistant 2026.9.1 ships.
The number of columns is not the integration's to set either. The page computes
it as ⌊(width + 16) / 336⌋ — each column at least 320 px — from the width of
its own content area, so it follows the browser window. Below about 990 px of
content width you get two columns: narrow the window, keep Home Assistant's
sidebar expanded (it takes its width from the content area), or zoom the
browser in.
Every entity carries an explanation, shown on the NIBE page and in its attributes. For the values a person actually looks at or changes - all the controls and the readings that say how the pump is doing - the explanation is written out: what it is, what it does to the house, and what happens if you change it. The heating curve says to move it one step and wait a day; the degree minutes explain what the number counts and why it goes negative.
They are written per family rather than per register, because an installation only shows the registers its own accessories bring: one house has an exhaust air module, the next a pool, a third eight climate systems. So the pool's start and stop temperatures, the ERS module's settings and the eighth climate system's room sensor are all explained here although no pump this was built against has them - a scan that finds an accessory brings explained values with it. That covers about seven eighths of the registers in NIBE's maps for both series, and a little more of the ones you can set.
The rest is built from NIBE's own title and a glossary of designations, which
is how a register NIBE documents as (EB100-EP15-BT28) still says what it
measures. A register whose meaning could not be verified keeps that built text
rather than a guess, and a service value from the pump's own electronics says
that is what it is rather than being given an invented purpose.
Alarm registers read as the alarm's text — Inget larm, or Fel fasföljd alt.
saknad fas har uppmätts. — from NIBE's published S-series alarm list, with the
number kept as an alarm_code attribute for automations. NIBE reuses some codes
for more than one alarm (237 is a short running time in hot water/heating, in
the compressor or in cooling), so those carry every meaning the list gives.
The F-series uses a different list, whose numbers mean other things: 163 is a
missing phase on an S-series pump and a hot condenser inlet on an F1255. Its
alarms read from NIBE's list for products with the Emmy display, 328 codes in
Swedish, via tools/extract_nibe_alarms_fseries.py. The English titles come
from NIBE's older English edition, and only where it agrees with the Swedish:
between the two, NIBE gave some numbers new meanings - 150 went from High
condensor out to a temporary high pressure alarm - so a code whose English
title disagrees, or is missing, reads as Alarm 150 in English rather than as
the wrong alarm.
Registers with a value table read as words too, and settings with one become dropdowns. The tables come from NIBE's own register documentation, which is more complete and more precise than the library's: it labels the diverter valve heating/hot water where the library says off/on, and it documents the brine pump's intermittent / continuous / 10 days continuous, which the library leaves as a bare number. See docs/nibe-references.md.
The pump keeps two lifetime energy counters on Modbus — register 33822, heat delivered, and 33824, electricity consumed — and they become the Total produktion and Total förbrukning sensors, ready for Home Assistant's Energy dashboard. On the development unit they read the same figures myUplink's cloud reports for the pump, to the tenth of a kWh.
Their ratio is the coefficient of performance, computed the same way as in the CTC integration so the two are comparable:
- COP, dygn — over the last day, from a sample 20–30 hours old. Appears about a day after setup.
- COP, år — over a rolling year. Until a year of samples exists it shows the
lifetime figure, and its
basisattribute says so. - COP, livstid — over everything the counters have counted, which on an S-series pump is since it was installed. The one span that needs no waiting, so it is the figure the overview shows; the rolling year is the one to compare between years, and stands on the performance tab until it has a year behind it. On the F-series it is named COP, sedan start, because there the electricity is counted from the day this integration was installed.
An F-series pump has no electricity counter, which is why it had no COP. Its own heat meters do count what it has delivered, and one model - the F730 - counts the electricity too, in three registers for heating, hot water and ventilation; where those are, they are read and nothing is estimated. On every other F-series the electricity is counted here instead: the pump reports the two things that use it — what the compressor draws and what the immersion heater draws, in kilowatts — and how fast its two circulation pumps are running, and its own heat meters count the heat it has delivered. Adding the power up over time is what a meter does, and that is what Förbrukad el (beräknad) is:
| Part | Where the figure comes from |
|---|---|
| Compressor | The inverter's own reading, register 43141 |
| Immersion heater | The pump's own reading, register 43084 |
| Circulation pumps | Their speed, and NIBE's watt figures for that size of pump: an F1255-16 brine pump runs 20–180 W, the 6 kW one 10–87 W. Power follows the cube of the speed |
| Control system | 15 W, an estimate; NIBE publishes none |
The compressor's figure is good, the pumps are a model and the electronics an estimate, so the total lands a few percent out — which is what a COP read to a tenth can carry. The sensor keeps each part as its own attribute, so a figure that looks wrong can be taken apart rather than believed. Both counters start from the same moment: the pump's heat meters have run since it was installed, so the heat is counted from where they stood when this one began.
For an exact figure, put an electricity meter on the pump's circuit — nothing here stops a measured sensor being used beside it.
The anonymous report is stricter than the sensor: it sends a yearly figure only once it really covers a year, since the lifetime figure under the yearly name would be a different number wearing the wrong label.
Earlier history is imported automatically. If Home Assistant already
recorded these two counters through another integration — myUplink reports
them as Tot. produktion / Tot. konsumtion — that history is read into the
COP once, at startup. Nothing is configured: the series are found because
their latest values match the pump's own counters, and both have to come from
the same device, which rules out a household meter that merely happens to sit
near the pump's consumption. The daily figure is then there at once, and a real
yearly figure arrives a year after the history begins rather than a year after
installation. The nibe_local_easyconf.import_energy_history service runs it
again, for instance after adding a source.
A select, Värmeläge (Heating mode), offers blocked, eco, normal and
boost: exactly the ids an energy manager such as
EMS Steward writes to the heat pump it is
bound to, shown in the interface as words. No NIBE map has a register for it,
so the select steers the one lever every map has, the heat offset of climate
system 1 - temperatur in menu 1.1, register 47011 on the F-series and 40031
on the S-series:
| Mode | Offset written |
|---|---|
boost |
normal + 1 |
normal |
normal |
eco |
normal − 1 |
blocked |
normal − 3 |
The steps are set under Configure. On an F1255 one step moves the supply temperature 2.5 °C at every outdoor temperature (NIBE's installer manual IHB SE 1614-2). Heating is never switched off: a blocked house coasts on its thermal mass with the minimum supply temperature still in force, the bounded set-back EMS asks of a heat pump. The defaults are EMS's own thermostat set-backs (+1, −1.5 and −3 °C) taken as steps.
Normal is learned: the offset the pump had when the select first saw it. An
offset changed anywhere else - on the display, in myUplink, through the
offset's own entity - is taken as meant. In normal it becomes the new normal
and the other modes follow it; in any other mode it holds until the mode next
changes. The select keeps showing the mode last chosen, so an energy manager
does not undo a change made by hand, and its attributes show the normal offset
and what each mode writes.
NIBE's own mechanism would be SG Ready, but on these pumps that is two contacts on the input board rather than a register, and the F-series' smart home mode and smart price adaption are read-only over MODBUS 40.
On the F-series a switch, Varmvattenboost (Hot water boost), is the one to bind as an energy manager's water heater, with EMS's water heater is a boost ticked. On writes temporary lux's One time increase (engångshöjning) to register 48132, off writes Off. Those are the values myUplink's own Tillfällig lyx switch was seen writing on the F1255-16, so a plan tuned on the cloud switch behaves the same through the gateway. A 3, 6 or 12 hour lux started at the pump reads as on.
The S-series calls its boost More hot water (40698), and neither the maps nor NIBE's register documentation give its values, so it has no switch yet.
Setting the integration up adds a NIBE entry to the sidebar on its own — nothing to configure. The page carries NIBE's own mark beside the pump's name; the sidebar entry keeps a heat pump icon, because Home Assistant's sidebar takes an icon's name rather than a picture. The page has four tabs.
Overview. What the pump is doing right now: the alarm, the modes it is running in and the day's coefficient of performance as chips; beside them the controls worth reaching for without going further - heating and hot water mode, holiday, the exhaust air module's fan - and the last day's temperatures as a graph. Under that, the key figures: the circuit's temperatures, the compressor, the energy counters and the year's coefficient of performance. Every one of them carries the same ⓘ as the controls do.
Controls. Everything writable, as the thing it is: a dropdown for a mode, a slider for a setting with a range you can aim at, a field for one that runs from −3000 to 3000, a toggle for a switch. They are grouped by what they do to the house — heating, hot water, operation, fans and pumps — rather than by entity domain, and each keeps its explanation behind the ⓘ. A value written here goes straight to the pump; the page never waits for a dialog.
Performance. The graphs in full: the circuit's temperatures and the compressor over the last day, the energy counters per day for a month, and the coefficient of performance. They are drawn by Home Assistant's own history and statistics cards, and a pump that reports none of it simply has no such tab.
All values, last. It opens with accessories registered: what the pump answers about its own modules - a pool, an exhaust air module, a room unit, an extra climate system, a boiler control - the ones it says no to as well, since that is half the answer. NIBE has no register for MODBUS 40 itself; that these values arrive at all is the word that it is switched on in menu 5.2. The pump's enabled entities two to a row with room for the whole name, grouped the way the device page groups them, with a filter that searches names and explanations. Hover a row for its explanation; tap it and the explanation is written out underneath, along with NIBE's original title and the register number, because a hover text alone never reaches a phone or a screen reader.
A sidebar panel rather than a generated dashboard, deliberately: Home Assistant
keeps its dashboards collection in a local variable inside the Lovelace
component and exposes it nowhere, so creating one from an integration means
reaching into internals that move between releases. panel_custom is the
public way to add a page, and it is how HACS itself gets into the sidebar.
No existing dashboard is ever touched.
The same view is available as a card for your own dashboards — the resource is registered automatically on storage-mode Lovelace:
type: custom:nibe-easyconf-card
device_id: <optional, when you have more than one pump>
tabs: [overview, controls, performance, values] # optional, this is the defaultIn YAML mode, add /nibe_local_easyconf/nibe-easyconf-card.js as a module
resource yourself.
You can. The pump was measured accepting 14 simultaneous TCP connections, and read latency did not degrade under five parallel clients (median 37 ms alone, 21 ms under load). Concurrent reads are safe.
Concurrent writes are not: if two integrations both write the same setting they will fight. Keep writes in one place.
The integration sends one report per day to https://stats.rnet.se: which version of the integration you run, your Home Assistant version and installation type, the country you have set in Home Assistant itself, an approximate position rounded to about 11 km, how many entities and devices it created, which model you picked, what kind of machine the readings say it is (ground source, exhaust air, hot water, cooling), how many registers the scan found and how many report a value, how many block reads a poll cycle costs, the update interval, the control board's software version, and how many poll cycles failed.
When the serial is known it also carries the article number, among the models, and the year and week of manufacture. The exact day stays local, and the serial's sequence number — the one part that identifies a single machine — never leaves the house.
A contract test pins every field against the backend's own validation rules.
Three fields in v1.3.0 were silently lost or mangled on arrival, among them the
article number, which the backend's four-digit product_code range clamped to
9999 without an error; the test now fails on exactly that class of mistake.
The register counts are the reason this exists. NIBE's published maps do not say which registers a given machine implements, and no single installation can tell you either — what the probing found came from one pump. Across a fleet those counts are how the maps get better.
It never sends a name, an address, an exact position, a serial number, an entity name, which registers were found, or any reading from the house. Nothing is finer-grained than one day, and your IP address is not stored.
It is on by default. Turn it off under Settings → Devices & services → Nibe Local Easyconf → Configure; doing so also erases what has already been sent, rather than merely going quiet.
What is collected and why: https://stats.rnet.se/integritet
- The F-series is supported through a NibeGW gateway rather than through NIBE's own MODBUS 40 accessory, and its coefficient of performance rests on a counted rather than a measured electricity figure; see what is different. About a quarter of its alarms have no English title, for the reason given under alarms and values in words.
- Register names come from NIBE's published maps, which are in English. Titles are translated to Swedish only where the whole phrase is recognised — a partial translation reads worse than the original.
- Descriptions decode component designations that could be pinned down from NIBE's own data. Unknown codes get a structural description rather than a guess.
- NIBE's MODBUS 40 accessory as an alternative to the gateway. The same pump protocol behind a real MODBUS 40, reached as Modbus RTU through a TCP-to-RTU converter: holding registers only, addressed by their full number (40004 is 0x9C44) before MODBUS 40 version 10, writes as Write Multiple Registers only, and 2.1 s per register outside LOG.SET.
- Hold stats.py's own keys to the backend as well. The contract test checks
what
stats_extrabuilds, but not the keysstats.pyadds itself (ha_id,log_errorsandlog_warningssince 1.7.2). The backend rejects a whole report over one unknown top-level key, so a key it does not know yet would silence every report without an error on this side.
Register maps come from the nibe library
(LGPL-3.0), declared as a dependency in manifest.json and installed by Home
Assistant. No LGPL code is copied into this repository.
NIBE and NIBE's mark belong to NIBE Energy Systems. The icon in brand/ is
the one Home Assistant publishes for its own nibe_heatpump integration, and
is used here for the same purpose: to show which make of pump this is about.
Home Assistant reads that folder itself for the integration's icon — since
2026.3 a custom integration carries its own brand images rather than submitting
them to a catalogue — and the NIBE page wears the same file beside the pump's
name. This project is not made by, endorsed by or affiliated with NIBE.