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.
Configuration variables
Section titled “Configuration variables”-
modbus_id (Optional, ID): Manually specify the ID of the
modbushub. -
address (Required, integer): The Modbus address of the server device. Address
0is 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_valueinstead of triggering an exception. Defaults to65535 - 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 to0.
- enabled (Optional, boolean): Whether to enable the courtesy response feature.
Defaults to
-
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_tS_WORD: signed 16 bit integer, 1 register,int16_tU_WORD_S: unsigned 16 bit integer, 1 register, byte-swapped,uint16_tS_WORD_S: signed 16 bit integer, 1 register, byte-swapped,int16_tU_DWORD: unsigned 32 bit integer, 2 registers,uint32_tS_DWORD: signed 32 bit integer, 2 registers,int32_tU_DWORD_R: unsigned 32 bit integer, 2 registers, low word first,uint32_tS_DWORD_R: signed 32 bit integer, 2 registers, low word first,int32_tU_QWORD: unsigned 64 bit integer, 4 registers,uint64_tS_QWORD: signed 64 bit integer, 4 registers,int64_tU_QWORD_R: unsigned 64 bit integer, 4 registers, low word first,uint64_tS_QWORD_R: signed 64 bit integer, 4 registers, low word first,int64_tFP32: 32 bit IEEE 754 floating point, 2 registers,floatFP32_R: 32 bit IEEE 754 floating point, 2 registers, low word first,float
WARNING
U_WORD_SandS_WORD_Sare 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_Ssuffix is not the same as_R(word order reversed / low word first across multiple registers).NOTE
The
_Rsuffix 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.
addressis provided with this register’s own start address, which is not necessarily the address the client asked for: underallow_partial_reada request may start inside the value. Returning an empty optional (return {};) declines the read, and the client’s whole request is answered with aSERVICE_DEVICE_FAILURE(code4) 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 plainreturn <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
xof the appropriate type (uint16_t,int32_t, etc, see above) is provided with the value, as well asaddresscontaining the address of this register. You must returntrueif the operation was successful,falseotherwise, in which case aSERVICE_DEVICE_FAILURE(code4) exception will be sent to the client.NOTE
Before any
write_lambdaruns, the whole request is validated: every targeted register must exist and be writable, or it is rejected with anILLEGAL_DATA_ADDRESS(code2) 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 withILLEGAL_DATA_VALUE(code3). Once these checks pass, the writes are applied in order; if awrite_lambdathen returnsfalse, the registers written before it keep their new values and the request answersSERVICE_DEVICE_FAILURE(code4). -
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. Whenfalsethe value is treated as atomic and any read that does not cover the whole value is rejected with anILLEGAL_DATA_ADDRESSexception. Has no effect on single-register types (U_WORD/S_WORD). Defaults tofalse.NOTE
Even with
allow_partial_readenabled, theread_lambdais 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 (0x0000–0xFFFF);
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 -
0x01and0x02to read,0x05and0x0Fto 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 thebitslist. Bit addresses are independent of register addresses: a bit at0x0000and a register at0x0000are 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.addressis provided with the address of this bit. Anything that converts toboolis accepted, and that conversion is implicit: an entity such asreturn id(my_switch);hands back a pointer, which is alwaystrue, and the bit reads1forever. Make sure the lambda returns the state you mean —return id(my_switch).state;for a switch, or the value of aglobalsbool. Returning an empty optional (return {};) declines the read, and the client’s whole request is answered with aSERVICE_DEVICE_FAILURE(code4) 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
boolvariablexis provided with the new value, as well asaddresscontaining the address of this bit. You must returntrueif the operation was successful,falseotherwise, in which case aSERVICE_DEVICE_FAILURE(code4) 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(code2) exception rather than answering with the bits that do exist.courtesy_responsedoes not apply here — it is only consulted for registers — so a client reading coils0x00-0x0Ffrom a server that defines only0x00and0x01gets 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 anILLEGAL_DATA_ADDRESS(code2) exception. Once that check passes, eachwrite_lambdais called in turn; if one of them returnsfalse, the write stops there — the bits already written stay changed, and the client’s request is answered with aSERVICE_DEVICE_FAILURE(code4) exception.NOTE
A server that configures no
bitsat all does not implement the coil and discrete-input functions: those requests are answered withILLEGAL_FUNCTION(code1) rather thanILLEGAL_DATA_ADDRESS(code2), which is what a request for an address that simply isn’t in a populatedbitslist gets. The same holds the other way round: a bits-only server with noregistersanswersILLEGAL_FUNCTIONto register requests — to reads unlesscourtesy_responseis enabled, and to writes always, sincecourtesy_responseonly 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 setallow_partial_read— the client receives an exception response even though the write has already taken effect. - If a
write_lambdarejects 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.
Example
Section titled “Example”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_sensThe 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;