Skip to content
Get started

Modbus Server

The modbus_server component creates an RS485 connection to let your ESPHome node act as a Modbus server, allowing a Modbus client to read data (like sensor values) from your ESPHome node.

You must set the role attribute of the Modbus to server.

NOTE

Modbus broadcast writes use address 0. They are delivered to every registered server device on the same hub and do not send a response. Only write function codes are handled as broadcasts - 0x06 and 0x10 for registers, 0x05 and 0x0F for coils; all other requests addressed to 0, including reads, are ignored.

See Modbus for RS485 hardware setup instructions.

  • modbus_id (Optional, ID): Manually specify the ID of the modbus hub.

  • address (Required, integer): The Modbus address of the server device. Address 0 is reserved for broadcasts and cannot be assigned to a server device.

  • courtesy_response (Optional): Configuration block to enable the courtesy response feature when the device is acting as a Modbus server.

    • enabled (Optional, boolean): Whether to enable the courtesy response feature. Defaults to false.
    • register_last_address (Optional, integer): The highest Modbus register address (inclusive) up to which undefined registers are allowed to be read and will be padded with a default value. Any read request that includes undefined registers within this range will return the value specified by register_value instead of triggering an exception. Defaults to 65535
    • register_value (Optional, integer): The 16-bit value (range: 0–65535) to return for undefined registers within the address range defined by register_last_address. Defaults to 0.
  • registers (Optional): A list of registers that are responded to when acting as a server.

    • address (Required, integer): start address of the first register in a range.

    • value_type (Optional): data type of the Modbus register data. The default data type for Modbus is a 16-bit integer in big endian format (network byte order, MSB first).

      • U_WORD : unsigned 16 bit integer, 1 register, uint16_t
      • S_WORD : signed 16 bit integer, 1 register, int16_t
      • U_WORD_S : unsigned 16 bit integer, 1 register, byte-swapped, uint16_t
      • S_WORD_S : signed 16 bit integer, 1 register, byte-swapped, int16_t
      • U_DWORD : unsigned 32 bit integer, 2 registers, uint32_t
      • S_DWORD : signed 32 bit integer, 2 registers, int32_t
      • U_DWORD_R : unsigned 32 bit integer, 2 registers, low word first, uint32_t
      • S_DWORD_R : signed 32 bit integer, 2 registers, low word first, int32_t
      • U_QWORD : unsigned 64 bit integer, 4 registers, uint64_t
      • S_QWORD : signed 64 bit integer, 4 registers, int64_t
      • U_QWORD_R : unsigned 64 bit integer, 4 registers, low word first, uint64_t
      • S_QWORD_R : signed 64 bit integer, 4 registers, low word first, int64_t
      • FP32 : 32 bit IEEE 754 floating point, 2 registers, float
      • FP32_R : 32 bit IEEE 754 floating point, 2 registers, low word first, float

      WARNING

      U_WORD_S and S_WORD_S are a rare, non-standard configuration: the two bytes within one 16-bit register are reversed (LSB first on the wire within that register). Modbus registers are big endian (MSB first) per the specification. The _S suffix is not the same as _R (word order reversed / low word first across multiple registers).

      NOTE

      The _R suffix means word order is reversed (low word first). It is not little-endian byte order of the whole value; bytes within each register remain MSB first.

      Defaults to U_WORD.

    • read_lambda (Required, lambda): Lambda that returns the value of this register. address is provided with this register’s own start address, which is not necessarily the address the client asked for: under allow_partial_read a request may start inside the value. Returning an empty optional (return {};) declines the read, and the client’s whole request is answered with a SERVICE_DEVICE_FAILURE (code 4) exception instead - any other register in the same request goes unread. This is useful when the value isn’t available right now, for example while a sensor is still warming up. A plain return <value>; still works as before and does not need to change.

    • write_lambda (Optional, lambda): Lambda that sets the value of this register. A variable x of the appropriate type (uint16_t, int32_t, etc, see above) is provided with the value, as well as address containing the address of this register. You must return true if the operation was successful, false otherwise, in which case a SERVICE_DEVICE_FAILURE (code 4) exception will be sent to the client.

      NOTE

      Before any write_lambda runs, the whole request is validated: every targeted register must exist and be writable, or it is rejected with an ILLEGAL_DATA_ADDRESS (code 2) exception and nothing is written. A register write that does not supply a complete value for a wide type (U_DWORD, FP32, …) — including a single-register write (0x06) to one — is rejected with ILLEGAL_DATA_VALUE (code 3). Once these checks pass, the writes are applied in order; if a write_lambda then returns false, the registers written before it keep their new values and the request answers SERVICE_DEVICE_FAILURE (code 4).

    • allow_partial_read (Optional, boolean): For a multi-register value type (U_DWORD, S_QWORD, FP32, …), allow a read request to cover only part of the value — starting inside it or stopping short of its end — and return just the requested registers. When false the value is treated as atomic and any read that does not cover the whole value is rejected with an ILLEGAL_DATA_ADDRESS exception. Has no effect on single-register types (U_WORD/S_WORD). Defaults to false.

      NOTE

      Even with allow_partial_read enabled, the read_lambda is still evaluated once per request, so the registers returned within a single request are always consistent with each other. A client that reads the parts of a value in separate requests may observe a value that changed in between — this is inherent to register-by-register Modbus access.

      This option affects reads only. Writes are always atomic: a write must cover a whole value, since a partial value cannot be reconstructed for the write_lambda.

NOTE

Each register entry occupies multiple consecutive addresses based on its value_type (1 for *_WORD, 2 for *_DWORD/FP32, 4 for *_QWORD). Register address ranges within one modbus_server must not overlap and must fit within the 16-bit address space (0x00000xFFFF); the configuration is rejected otherwise.

  • bits (Optional): A list of bits that are responded to when acting as a server. Bits answer the coil and discrete-input function codes - 0x01 and 0x02 to read, 0x05 and 0x0F to write - from one shared table: a coil and a discrete input at the same address are the same bit, the same way holding and input registers share the register table above. A client can read a bit as a coil or as a discrete input and gets the same value either way. Each bit address must be unique within the bits list. Bit addresses are independent of register addresses: a bit at 0x0000 and a register at 0x0000 are different things and do not collide.

    • address (Required, integer): the address of this bit.

    • read_lambda (Required, lambda): Lambda that returns the value of this bit, as a bool. address is provided with the address of this bit. Anything that converts to bool is accepted, and that conversion is implicit: an entity such as return id(my_switch); hands back a pointer, which is always true, and the bit reads 1 forever. Make sure the lambda returns the state you mean — return id(my_switch).state; for a switch, or the value of a globals bool. Returning an empty optional (return {};) declines the read, and the client’s whole request is answered with a SERVICE_DEVICE_FAILURE (code 4) exception instead of a value - the other bits in the same request go unread, so this is not a per-bit “not ready” signal.

    • write_lambda (Optional, lambda): Lambda that sets the value of this bit. A bool variable x is provided with the new value, as well as address containing the address of this bit. You must return true if the operation was successful, false otherwise, in which case a SERVICE_DEVICE_FAILURE (code 4) exception will be sent to the client. If this is not set, the bit can be read but not written.

    NOTE

    A read is all-or-nothing: if any address in the requested span has no configured bit, the whole request is rejected with an ILLEGAL_DATA_ADDRESS (code 2) exception rather than answering with the bits that do exist. courtesy_response does not apply here — it is only consulted for registers — so a client reading coils 0x00-0x0F from a server that defines only 0x00 and 0x01 gets an exception either way. Define every address a client will ask for.

    NOTE

    A write covering several bits (for example a “write multiple coils” request) is atomic in the same way as a multi-register write: every targeted bit must exist and have a write_lambda, or the whole request is rejected before anything is changed, with an ILLEGAL_DATA_ADDRESS (code 2) exception. Once that check passes, each write_lambda is called in turn; if one of them returns false, the write stops there — the bits already written stay changed, and the client’s request is answered with a SERVICE_DEVICE_FAILURE (code 4) exception.

    NOTE

    A server that configures no bits at all does not implement the coil and discrete-input functions: those requests are answered with ILLEGAL_FUNCTION (code 1) rather than ILLEGAL_DATA_ADDRESS (code 2), which is what a request for an address that simply isn’t in a populated bits list gets. The same holds the other way round: a bits-only server with no registers answers ILLEGAL_FUNCTION to register requests — to reads unless courtesy_response is enabled, and to writes always, since courtesy_response only covers reads.

Read/Write Multiple Registers (Function Code 0x17)

Section titled “Read/Write Multiple Registers (Function Code 0x17)”

When acting as a server, modbus_server also answers function code 0x17 (Read/Write Multiple Registers), which combines a write and a read into a single request. It reuses the same write_lambda and read_lambda as the standalone write and read functions, so any writable and readable register supports it without additional configuration. allow_partial_read and courtesy_response apply to the read half exactly as they do to a standalone read.

WARNING

The Modbus Application Protocol Specification (section 6.17) mandates that the write is performed before the read, and neither half is rolled back when the request subsequently fails:

  • If the read half fails — for example because it targets an address that is neither mapped nor covered by courtesy_response, or covers only part of a multi-register value that does not set allow_partial_read — the client receives an exception response even though the write has already taken effect.
  • If a write_lambda rejects a value part-way through a multi-register write, the registers written before it keep their new values. Unwritable addresses and incomplete values are rejected before anything is written, and the read half is skipped whenever the write half fails.

A client that treats the exception as “nothing happened” and retries the request will apply the write a second time — harmless for idempotent registers, but potentially problematic for command or counter registers where each write has a side effect.

The following code allows a Modbus client to read a sensor value from your ESPHome node, that the node itself read from some local sensor.

uart:
- id: uart_modbus_server
tx_pin: GPIOXX
rx_pin: GPIOXX
modbus:
- uart_id: uart_modbus_server
id: modbus_server
role: server
modbus_server:
- modbus_id: modbus_server
address: 0x4
registers:
- address: 0x0002
value_type: U_DWORD_R
read_lambda: |-
return id(uptime_sens).state;
sensor:
- platform: uptime
id: uptime_sens

The following code serves two bits: a read-only bit reporting a relay’s state, and a read/write bit that lets a Modbus client both read and control whether a pump is running.

uart:
- id: uart_modbus_server
tx_pin: GPIOXX
rx_pin: GPIOXX
modbus:
- uart_id: uart_modbus_server
id: modbus_server
role: server
binary_sensor:
- platform: gpio
id: relay_state
pin: GPIOXX
globals:
- id: pump_running
type: bool
initial_value: 'false'
modbus_server:
- modbus_id: modbus_server
address: 0x4
bits:
- address: 0x00
read_lambda: |-
return id(relay_state).state;
- address: 0x01
read_lambda: |-
return id(pump_running);
write_lambda: |-
id(pump_running) = x;
return true;