[modbus_controller] Apply the write offset byte-accurately on switch and output (#18787)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Bonne Eggleston
2026-08-27 08:43:51 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent 51105a25db
commit 18b7002604
8 changed files with 130 additions and 28 deletions
@@ -278,6 +278,21 @@ def _final_validate(config: ConfigType) -> None:
FINAL_VALIDATE_SCHEMA = _final_validate
def reject_odd_holding_write_offset(config: ConfigType) -> ConfigType:
"""Reject an odd byte offset on a holding-register write entity.
A 16-bit register write cannot target half a register, so the residual byte is inexpressible.
"""
key = CONF_BYTE_OFFSET if CONF_BYTE_OFFSET in config else CONF_OFFSET
if config.get(key, 0) % 2:
raise cv.Invalid(
f"An odd '{key}' cannot be used with holding-register writes: a 16-bit register "
"write cannot target half a register. Use an even offset, or fold it into 'address'",
path=[key],
)
return config
def modbus_calc_properties(config: ConfigType) -> tuple[int, int]:
byte_offset = 0
reg_count = 0
@@ -14,6 +14,7 @@ from .. import (
SensorItem,
modbus_calc_properties,
modbus_controller_ns,
reject_odd_holding_write_offset,
)
from ..const import (
CONF_CUSTOM_COMMAND,
@@ -53,23 +54,26 @@ CONFIG_SCHEMA = cv.typed_schema(
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
"holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_PDU): cv.invalid(
"custom_pdu is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
"holding": cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_PDU): cv.invalid(
"custom_pdu is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid(
"custom_command is not supported for outputs; use a write_lambda instead"
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
reject_odd_holding_write_offset,
),
},
lower=True,
@@ -12,7 +12,8 @@ class ModbusFloatOutput final : public output::FloatOutput, public Component, pu
public:
ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) {
this->register_type = modbus::EntityType::HOLDING;
this->set_address(start_address + offset);
// A byte offset folds into the address as whole registers; odd offsets are rejected at validation.
this->set_address(start_address + offset / 2);
this->set_offset_from_start_address(0);
this->bitmask = 0xFFFFFFFF;
this->register_count = register_count;
@@ -11,6 +11,7 @@ from .. import (
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
reject_odd_holding_write_offset,
validate_custom_pdu_item,
validate_modbus_register,
)
@@ -31,6 +32,14 @@ ModbusSwitch = modbus_controller_ns.class_(
"ModbusSwitch", cg.Component, switch.Switch, SensorItem
)
def _validate_holding_offset(config: ConfigType) -> ConfigType:
# Only a holding-register switch folds the byte offset into a 16-bit register write.
if config.get(CONF_REGISTER_TYPE) == "holding":
reject_odd_holding_write_offset(config)
return config
CONFIG_SCHEMA = cv.All(
switch.switch_schema(ModbusSwitch, default_restore_mode="DISABLED")
.extend(cv.COMPONENT_SCHEMA)
@@ -44,6 +53,7 @@ CONFIG_SCHEMA = cv.All(
}
),
validate_modbus_register,
_validate_holding_offset,
)
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
@@ -18,8 +18,13 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens
this->bitmask = bitmask;
this->sensor_value_type = SensorValueType::BIT;
this->register_count = 1;
if (register_type == modbus::EntityType::HOLDING || register_type == modbus::EntityType::COIL) {
this->set_address(this->start_address + offset);
// A holding byte offset folds into the address as whole registers (odd offsets are rejected at
// validation: a 16-bit register write cannot target half a register); a coil offset is a coil count.
if (register_type == modbus::EntityType::HOLDING) {
this->set_address(start_address + offset / 2);
this->set_offset_from_start_address(0);
} else if (register_type == modbus::EntityType::COIL) {
this->set_address(start_address + offset);
this->set_offset_from_start_address(0);
}
this->force_new_range = force_new_range;