---
title: "TapHome to Modbus RTU / ASCII / TCP"
description: "Integrate TapHome into a BMS or SCADA over Modbus: a worked example with the register map on the TapHome side, the point list for the master and a Python example."
url: "https://taphome.com/en/docs/expose-devices/modbus/"
language: en
lastmod: 2026-11-09
generator: hugo
ai:
  topics: ["modbus-integration","expose-devices","bms-integration","modbus-tcp","rs485-communication","system-integration"]
  questions: ["How do I integrate TapHome into a BMS or SCADA system?","How do I expose TapHome devices via Modbus?","What does the Modbus register map of TapHome look like?","Which register address does the BMS use for an exposed value?","What register types and data types can an exposed value use?","What does the validity register return?","How does a Modbus master trigger a device action such as a blind command?","Which Slave ID does TapHome answer?","Why does the master get Modbus exception 0x02 or 0x0A?","What changed in Modbus slave interfaces with TapHome 2026.1?"]
---

# TapHome to Modbus RTU / ASCII / TCP

TapHome as a Modbus slave for a BMS, SCADA or PLC over Modbus TCP, RTU or ASCII: pick the values and actions of each device, hand the register map to the master and read it there.

Last updated: 2026-11-09

TapHome can act as a Modbus slave (server), so a building management system (BMS), SCADA or PLC reads and controls TapHome devices over Modbus TCP, or over Modbus RTU or ASCII on RS485. In **Expose Devices** you pick the values and actions of each device, TapHome gives each one its own register, and the result is a register map you hand to whoever sets up the master. A second Core can read the devices this way too ([connecting multiple Cores](https://taphome.com/en/docs/configuration/multiple-cores/)).

## How It Works

1. **On the Core**, add a Modbus interface and pick what the master may see and do: values (temperature, blinds level, switch state …) and actions (blind commands, toggle …).
2. **The register map** is built as you pick: every value gets a register range at the next free address, with a validity register right after it.
3. **On the master**, the integrator enters the same registers as points or tags. Reading returns the current value; writing changes the device as a manual change, like in the app.

The Core keeps running its own modules and smart rules; Modbus only shares the values and actions you picked.

## Example: Blinds and Thermostat for a BMS

The BMS of a building reads the room temperature, sets the thermostat set point and can stop the blinds. On the Core, in **Expose Devices**, select **Add new interface** → **Modbus TCP**, then add the blinds and the thermostat. This is the complete configuration on the TapHome side — click a register or **Add device** to see the next step:



1. **IP address of the Core** — the BMS connects to it. Give the Core a fixed address, for example a DHCP reservation in the router.
2. **Slave ID** — the BMS must send the same Unit ID; a request with another one gets exception 0x0A.
3. **A value** takes two registers: the value itself and the validity register after it. *×100* is the multiplier: blinds level 0.55 reads as 55.
4. **An action** has one register the BMS writes to. Stop is a coil: writing ON stops the blinds.
5. **Register settings** — register type, index, data type, multiplier, byte order and the validity register. The defaults usually fit; change the index when the BMS expects a fixed layout.
6. **Add device** — check the values and actions of the device; **OK** adds them at the next free addresses. Writable values get a holding register (H) or coil (C), read-only values an input register (A) or discrete input (D).

## On the BMS or SCADA Side

The master needs the IP address and port of the Core (or the RS485 line settings), the Slave ID and the point list. For the example above:

**Point list**

| Point                 | TapHome register | 1-based notation | Function code     | Value     |
|-----------------------|------------------|------------------|-------------------|-----------|
| Blinds level          | H 0              | 40001            | 03 read, 06 write | 0–100     |
| Blinds level validity | H 1              | 40002            | 03                | 0 = valid |
| Slats angle           | H 2              | 40003            | 03 read, 06 write | 0–100     |
| Slats angle validity  | H 3              | 40004            | 03                | 0 = valid |
| Stop blinds           | C 4              | 00005            | 05 write          | ON = stop |
| Temperature           | A 5              | 30006            | 04                | ÷ 10 = °C |
| Temperature validity  | A 6              | 30007            | 04                | 0 = valid |
| Set point             | H 7              | 40008            | 03 read, 06 write | ÷ 10 = °C |
| Set point validity    | H 8              | 40009            | 03                | 0 = valid |

All registers are Int16 (a coil is one bit). H = holding register, A = input register, C = coil.

> TapHome numbers registers from 0, as they are sent in the request. Masters that use the 1-based notation (40001 for holding register 0) are one higher — an offset of one is the most common cause of reading the wrong value.



**Python (pymodbus)**

Reading and writing the example with [pymodbus](https://github.com/pymodbus-dev/pymodbus) 3.16:

\```python from pymodbus.client import ModbusTcpClient client = ModbusTcpClient(&#34;192.168.1.50&#34;, port=502) client.connect() # Temperature: input register 5 (value × 10) and 6 (validity, 0 = valid) rr = client.read\_input\_registers(5, count=2, device\_id=1) value, validity = client.convert\_from\_registers(rr.registers, client.DATATYPE.INT16) if validity == 0: print(f&#34;Temperature: {value / 10} °C&#34;) # Set point 22.5 °C: holding register 7, written × 10 client.write\_register(7, 225, device\_id=1) # Stop the blinds: write ON to coil 4 client.write\_coil(4, True, device\_id=1) client.close() \`\`\`

Libraries for other languages: [NModbus4](https://github.com/NModbus4/NModbus4) (C#), [libmodbus](https://libmodbus.org) (C), [digitalpetri/modbus](https://github.com/digitalpetri/modbus) (Java), [goburrow/modbus](https://github.com/goburrow/modbus) (Go). [Simply Modbus](http://www.simplymodbus.ca/) explains the protocol.

The master should read the validity register together with the value (one request for both) and treat any code other than 0 as a fault — a lost device then shows as a fault in the BMS, not as a frozen value.

## Interfaces and Communication Properties

| Interface | Properties | Default |
|---|---|---|
| **Modbus TCP** | **TCP port**, **Slave ID** | port 502, the next free Slave ID on the port |
| **Modbus RTU**, **Modbus ASCII** | terminal BUS 1, BUS 2 or BUS 3, baud rate, data bits, parity, stop bits, **Slave ID** | no terminal, 9600 baud, 8 data bits (ASCII 7), parity None, 1 stop bit, Slave ID 1 |

- **Several masters over TCP:** several Modbus TCP interfaces can share one port if they have different Slave IDs — for example one for the BMS and one for a second Core, each with its own registers. Up to 10 connections at a time.
- **RS485:** an RTU or ASCII interface starts only after you pick a terminal, and one terminal carries one interface.
- **Slave ID** is 1 to 247. The interface answers only its own: Modbus TCP returns exception 0x0A to another Unit ID (also 0 or 255), RTU and ASCII do not answer frames for another address.

The list of interfaces shows conflicts, such as the same port and Slave ID twice or an interface without a terminal.

## Register Settings

| Setting | Values |
|---|---|
| **Register type** | **H** holding register, **A** input register, **C** coil, **D** discrete input. The master can write only H and C. |
| **Register index** | 0 to 65535, the address in the Modbus request. |
| **Data type** | Bool, Int16 and UInt16 take one register, Int32, UInt32 and Float two. C and D carry only Bool. |
| **Multiplier** | Read: register = value × multiplier. Write: value = register ÷ multiplier. Temperatures default to ×10, levels and percentages to ×100 (0.55 → 55). |
| **Threshold** | Instead of the multiplier for Bool on a numeric value: true when the value is at least the threshold; writing true to a coil sets the value to the threshold, false to 0. |
| **Endianness** | BigEndian or LittleEndian; **Byte swap** also swaps the bytes inside each register of Int32, UInt32 and Float. |

The interface detail marks registers whose ranges overlap. An overlapping value is not served — give each value its own range, including its validity register.

## Validity Register and Fallback Value

**Include validity register** (on C and D **Include validity bit**) is on by default. The read-only register right after the value tells the master whether the value is valid; when it is not, the value's registers return the **Fallback value** instead (for Float, NaN with **Use NaN as fallback**).

| Code | Meaning |
|---|---|
| 0 | valid |
| 1 | the value does not fit the data type (for example NaN in an integer type) |
| 2 | the device is unavailable or in error |
| 3 | the register settings are invalid |
| 5 | no value yet, for example right after the Core starts |

On C and D, the validity bit is 1 when the value is not valid.

## Actions

- **Action without parameters** (Stop, Toggle …) has one register on H or C. Writing 1 or more, or coil ON, triggers it; writing 0 returns exception 0x03.
- **Action with parameters** (adding to the blinds level, setting a range …) has one holding register per parameter, one after another. One write must cover all of them; a write of only some returns exception 0x03.
- Action registers are write-only — reading them returns exception 0x02.

## What the Master Gets

TapHome serves function codes 01, 02, 03, 04, 05, 06, 15 and 16. One request may read several values in a row; otherwise the master gets an exception:

| Exception | When |
|---|---|
| 0x01 | an unsupported function code |
| 0x02 | an address not assigned to any value, a range with a gap, a write to A or D, a read of an action register, a write to a validity register |
| 0x03 | a value the device cannot take, or an invalid action write; a write with several values is all or nothing |
| 0x04 | the device refused the write |
| 0x0A | Modbus TCP: another Slave ID (Unit ID) than the interface's |

Modbus TCP closes a connection idle for 15 minutes.

## Updating to 2026.1

> [!WARNING]
> When the control unit is updated to 2026.1, existing Modbus slave interfaces are converted automatically, keeping the register addresses and Slave ID. The Stop register of a KNX blind, of a Packet parser blind and **Awning** device, and of a virtual **Awning** device is not converted; a master that used it gets exception 0x02 at its address ([release notes 2026.1](https://taphome.com/en/docs/release-notes/2026-1/#expose-devices)).

Since 2026.1, Modbus TCP answers only its own Slave ID — if your master sends Unit ID 0 or 255, set it to the interface's Slave ID.

## Debugging Modbus RTU Connections

RS485 is a shared bus, so a PC with a USB to RS485 converter (for example [FTDI USB-RS485-WE-1800-BT](https://ftdichip.com/products/usb-rs485-we-1800-bt/)) and terminal software, such as [Terminal](http://www.der-hammer.info/terminal/) for Windows, can listen to the communication while you integrate.
