Compare commits

..

3 Commits

Author SHA1 Message Date
Jesse Hills f69fea037a Merge branch 'dev' into jesserockz-2026-342 2026-07-01 16:34:18 +12:00
Jesse Hills d2178a157b Merge branch 'dev' into jesserockz-2026-342 2026-06-30 10:34:57 +12:00
Jesse Hills 49d6718345 Mark user-configurable classes as final (part 17/21)
Add the C++ `final` specifier to leaf, user-configurable component classes and
automation action/trigger/condition primitives so that classes meant to be
terminal cannot be subclassed by external components. Only classes never used as
a base anywhere in the tree are marked. Part 17 of 21, split alphabetically by
component (ssd1351_spi .. tem3200).
2026-06-15 13:22:16 +12:00
55 changed files with 134 additions and 481 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.25
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.23
RUN \
platformio settings set enable_telemetry No \
+2 -53
View File
@@ -8,10 +8,8 @@ from esphome.components.modbus.helpers import (
)
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from esphome.types import ConfigType
from .const import (
CONF_ALLOW_PARTIAL_READ,
CONF_COURTESY_RESPONSE,
CONF_READ_LAMBDA,
CONF_REGISTER_LAST_ADDRESS,
@@ -43,62 +41,17 @@ SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
}
)
# RAW has no numeric encoding, so it is not a valid server register type: a server value is produced by a
# lambda and encoded into registers, and on the server a RAW register would just be a single 16-bit word --
# use U_WORD for that. Restrict the choices to the encodable types.
SERVER_SENSOR_VALUE_TYPE = {
key: value for key, value in SENSOR_VALUE_TYPE.items() if key != "RAW"
}
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SERVER_SENSOR_VALUE_TYPE
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_ALLOW_PARTIAL_READ, default=False): cv.boolean,
}
)
def _validate_register_ranges(config: ConfigType) -> ConfigType:
# Each register occupies [address, address + register_count); the whole span must fit inside the 16-bit
# Modbus address space (0x0000-0xFFFF).
for register in config.get(CONF_REGISTERS, []):
address = register[CONF_ADDRESS]
register_count = TYPE_REGISTER_MAP[register[CONF_VALUE_TYPE]]
if address + register_count > 0x10000:
raise cv.Invalid(
f"Register at 0x{address:04X} spans {register_count} register(s) and runs past "
"the end of the 16-bit address space (0xFFFF)",
path=[CONF_REGISTERS],
)
return config
def _validate_no_overlapping_registers(config: ConfigType) -> ConfigType:
# Each register occupies [address, address + register_count). Reject configs where any two ranges
# overlap -- the same address twice, or a multi-register value straddling a neighbour -- since the
# server resolves a request by the value containing an address and overlaps are ambiguous.
spans = sorted(
(register[CONF_ADDRESS], TYPE_REGISTER_MAP[register[CONF_VALUE_TYPE]])
for register in config.get(CONF_REGISTERS, [])
)
for (address, register_count), (next_address, _) in zip(
spans, spans[1:], strict=False
):
if next_address < address + register_count:
raise cv.Invalid(
f"Register address 0x{next_address:04X} overlaps the register at 0x{address:04X}, "
f"which spans {register_count} register(s); each register's address range must be unique",
path=[CONF_REGISTERS],
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -109,12 +62,10 @@ CONFIG_SCHEMA = cv.All(
): cv.ensure_list(ModbusServerRegisterSchema),
}
).extend(modbus.modbus_device_schema(0x01, role="server")),
_validate_register_ranges,
_validate_no_overlapping_registers,
)
def _final_validate(config: ConfigType) -> ConfigType:
def _final_validate(config):
return modbus.final_validate_modbus_device("modbus_server", role="server")(config)
@@ -167,8 +118,6 @@ async def to_code(config):
),
)
)
if server_register[CONF_ALLOW_PARTIAL_READ]:
cg.add(server_register_var.set_allow_partial_read(True))
cg.add(var.add_server_register(server_register_var))
await cg.register_component(var, config)
return await modbus.register_modbus_server_device(var, config)
@@ -5,4 +5,3 @@ CONF_COURTESY_RESPONSE = "courtesy_response"
CONF_READ_LAMBDA = "read_lambda"
CONF_WRITE_LAMBDA = "write_lambda"
CONF_REGISTERS = "registers"
CONF_ALLOW_PARTIAL_READ = "allow_partial_read"
@@ -8,25 +8,6 @@ using modbus::helpers::registers_to_number;
static const char *const TAG = "modbus_server";
// The widest Modbus value type (QWORD) spans four registers.
static constexpr uint8_t MAX_REGISTERS_PER_VALUE = 4;
// number_to_payload() encodes the 64-bit value returned by read_lambda() into 16-bit registers, so the
// widest possible value spans exactly sizeof(int64_t) / sizeof(uint16_t) registers. Tie the bound to that
// source so a future wider value type -- which would require widening the encoded value itself -- can't
// silently overflow the value_words buffer below (StaticVector::push_back drops words past capacity).
static_assert(MAX_REGISTERS_PER_VALUE == sizeof(int64_t) / sizeof(uint16_t),
"MAX_REGISTERS_PER_VALUE must match the register span of the widest encodable value");
ServerRegister *ModbusServer::find_containing_register_(uint32_t address) const {
for (auto *server_register : this->server_registers_) {
if (address >= server_register->address &&
address < static_cast<uint32_t>(server_register->address) + server_register->register_count) {
return server_register;
}
}
return nullptr;
}
modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address,
uint16_t number_of_registers,
modbus::RegisterValues &registers) {
@@ -34,68 +15,42 @@ modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t sta
"Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.",
this->address_, start_address, number_of_registers);
const uint32_t end_address = static_cast<uint32_t>(start_address) + number_of_registers;
uint32_t current_address = start_address;
while (current_address < end_address) {
ServerRegister *server_register = this->find_containing_register_(current_address);
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
char value_buf[ServerRegister::FORMAT_VALUE_BUF_SIZE];
ESP_LOGV(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf)));
if (server_register == nullptr) {
// Unregistered address: optionally answer with the courtesy default, otherwise reject.
if (this->server_courtesy_response_.enabled &&
current_address <= this->server_courtesy_response_.register_last_address) {
ESP_LOGV(TAG, "No register at 0x%04X; returning courtesy default %" PRIu16 ".",
static_cast<uint16_t>(current_address), this->server_courtesy_response_.register_value);
registers.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // the courtesy default is always a single register
continue;
modbus::helpers::number_to_payload(registers, value, server_register->value_type);
current_address += server_register->register_count;
found = true;
break;
}
ESP_LOGW(TAG, "No register at 0x%04X and courtesy default not allowed. Sending exception response.",
static_cast<uint16_t>(current_address));
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
}
if (!server_register->read_lambda) {
// Registered but not readable (write-only); don't mask it with the courtesy default.
ESP_LOGW(TAG, "Register at 0x%04X is not readable. Sending exception response.", server_register->address);
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
if (!found) {
if (this->server_courtesy_response_.enabled &&
(current_address <= this->server_courtesy_response_.register_last_address)) {
ESP_LOGV(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %" PRIu16 ".",
current_address, this->server_courtesy_response_.register_value);
registers.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
}
}
// A multi-register value is normally atomic: the request must start at its first register and cover all of
// it. A value may opt in to partial reads, in which case the request may start inside it or stop short of
// its end and we return only the covered words.
const uint16_t value_offset = static_cast<uint16_t>(current_address - server_register->address);
const uint16_t words_available = static_cast<uint16_t>(server_register->register_count - value_offset);
const uint16_t words_wanted = static_cast<uint16_t>(end_address - current_address);
const uint16_t take = words_available < words_wanted ? words_available : words_wanted;
const bool clipped = value_offset != 0 || take != server_register->register_count;
if (clipped && !server_register->allow_partial_read) {
ESP_LOGW(TAG,
"Read clips the multi-register value at 0x%04X, which does not allow partial reads. "
"Sending exception response.",
server_register->address);
return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS;
}
int64_t value = server_register->read_lambda();
char value_buf[ServerRegister::FORMAT_VALUE_BUF_SIZE];
ESP_LOGV(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf)));
// Encode the whole value once (wire word order) and emit only the covered words. Slicing the encoded words
// handles the reversed value types for free, since number_to_payload already emits in wire order.
StaticVector<uint16_t, MAX_REGISTERS_PER_VALUE> value_words;
modbus::helpers::number_to_payload(value_words, value, server_register->value_type);
if (value_offset + take > value_words.size()) {
// The value encoded to fewer words than its register span (e.g. a RAW register); treat as a device fault.
ESP_LOGE(TAG, "Register at 0x%04X did not encode to %u registers", server_register->address,
server_register->register_count);
return ModbusExceptionCode::SERVICE_DEVICE_FAILURE;
}
for (uint16_t i = 0; i < take; i++) {
registers.push_back(value_words[value_offset + i]);
}
current_address += take;
}
return {};
@@ -84,13 +84,9 @@ class ServerRegister {
}
}
void set_allow_partial_read(bool allow_partial_read) { this->allow_partial_read = allow_partial_read; }
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
// When true, a read may cover only part of this multi-register value; otherwise it must read the whole value.
bool allow_partial_read{false};
ReadLambda read_lambda;
WriteLambda write_lambda;
};
@@ -115,8 +111,6 @@ class ModbusServer : public Component, public modbus::ModbusServerDevice {
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected:
/// Find the registered value whose register span contains address, or nullptr if none does.
ServerRegister *find_containing_register_(uint32_t address) const;
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Server courtesy response
+3 -3
View File
@@ -6,9 +6,9 @@
namespace esphome::ssd1351_spi {
class SPISSD1351 : public ssd1351_base::SSD1351,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_8MHZ> {
class SPISSD1351 final : public ssd1351_base::SSD1351,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_8MHZ> {
public:
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::st7567_i2c {
class I2CST7567 : public st7567_base::ST7567, public i2c::I2CDevice {
class I2CST7567 final : public st7567_base::ST7567, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
+3 -3
View File
@@ -6,9 +6,9 @@
namespace esphome::st7567_spi {
class SPIST7567 : public st7567_base::ST7567,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_8MHZ> {
class SPIST7567 final : public st7567_base::ST7567,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_8MHZ> {
public:
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
+3 -3
View File
@@ -26,9 +26,9 @@ const uint8_t CMD2_BKSEL = 0xFF;
const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10};
const uint8_t ST7701S_DELAY_FLAG = 0xFF;
class ST7701S : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
class ST7701S final : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
public:
void update() override { this->do_update_(); }
void setup() override;
+3 -3
View File
@@ -31,9 +31,9 @@ enum ST7735Model {
ST7735_INITR_18REDTAB = INITR_18REDTAB
};
class ST7735 : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
class ST7735 final : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr,
bool invert_colors);
+3 -3
View File
@@ -106,9 +106,9 @@ static const uint8_t ST7789_MADCTL_GS = 0x01;
static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR;
class ST7789V : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_20MHZ> {
class ST7789V final : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_20MHZ> {
public:
void set_model_str(const char *model_str);
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
+3 -3
View File
@@ -10,9 +10,9 @@ class ST7920;
using st7920_writer_t = display::DisplayWriter<ST7920>;
class ST7920 : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_200KHZ> {
class ST7920 final : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
public:
void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; }
void set_height(uint16_t height) { this->height_ = height; }
+1 -1
View File
@@ -27,7 +27,7 @@
namespace esphome::statsd {
class StatsdComponent : public PollingComponent {
class StatsdComponent final : public PollingComponent {
public:
~StatsdComponent();
@@ -5,7 +5,7 @@
namespace esphome::status {
class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent {
class StatusBinarySensor final : public binary_sensor::BinarySensor, public PollingComponent {
public:
void update() override;
@@ -7,7 +7,7 @@
namespace esphome::status_led {
class StatusLEDLightOutput : public light::LightOutput, public Component {
class StatusLEDLightOutput final : public light::LightOutput, public Component {
public:
void set_pin(GPIOPin *pin) { pin_ = pin; }
void set_output(output::BinaryOutput *output) { output_ = output; }
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::status_led {
class StatusLED : public Component {
class StatusLED final : public Component {
public:
explicit StatusLED(GPIOPin *pin);
+5 -5
View File
@@ -37,7 +37,7 @@ class Stepper {
uint32_t last_step_{0};
};
template<typename... Ts> class SetTargetAction : public Action<Ts...> {
template<typename... Ts> class SetTargetAction final : public Action<Ts...> {
public:
explicit SetTargetAction(Stepper *parent) : parent_(parent) {}
@@ -49,7 +49,7 @@ template<typename... Ts> class SetTargetAction : public Action<Ts...> {
Stepper *parent_;
};
template<typename... Ts> class ReportPositionAction : public Action<Ts...> {
template<typename... Ts> class ReportPositionAction final : public Action<Ts...> {
public:
explicit ReportPositionAction(Stepper *parent) : parent_(parent) {}
@@ -61,7 +61,7 @@ template<typename... Ts> class ReportPositionAction : public Action<Ts...> {
Stepper *parent_;
};
template<typename... Ts> class SetSpeedAction : public Action<Ts...> {
template<typename... Ts> class SetSpeedAction final : public Action<Ts...> {
public:
explicit SetSpeedAction(Stepper *parent) : parent_(parent) {}
@@ -77,7 +77,7 @@ template<typename... Ts> class SetSpeedAction : public Action<Ts...> {
Stepper *parent_;
};
template<typename... Ts> class SetAccelerationAction : public Action<Ts...> {
template<typename... Ts> class SetAccelerationAction final : public Action<Ts...> {
public:
explicit SetAccelerationAction(Stepper *parent) : parent_(parent) {}
@@ -92,7 +92,7 @@ template<typename... Ts> class SetAccelerationAction : public Action<Ts...> {
Stepper *parent_;
};
template<typename... Ts> class SetDecelerationAction : public Action<Ts...> {
template<typename... Ts> class SetDecelerationAction final : public Action<Ts...> {
public:
explicit SetDecelerationAction(Stepper *parent) : parent_(parent) {}
+3 -1
View File
@@ -9,7 +9,9 @@
namespace esphome::sts3x {
/// This class implements support for the ST3x-DIS family of temperature i2c sensors.
class STS3XComponent : public sensor::Sensor, public PollingComponent, public sensirion_common::SensirionI2CDevice {
class STS3XComponent final : public sensor::Sensor,
public PollingComponent,
public sensirion_common::SensirionI2CDevice {
public:
void setup() override;
void dump_config() override;
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::stts22h {
class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
class STTS22HComponent final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
+1 -1
View File
@@ -11,7 +11,7 @@ enum SensorType {
SUN_SENSOR_AZIMUTH,
};
class SunSensor : public sensor::Sensor, public PollingComponent {
class SunSensor final : public sensor::Sensor, public PollingComponent {
public:
void set_parent(Sun *parent) { parent_ = parent; }
void set_type(SensorType type) { type_ = type; }
+3 -3
View File
@@ -51,7 +51,7 @@ struct HorizontalCoordinate {
} // namespace internal
class Sun {
class Sun final {
public:
void set_time(time::RealTimeClock *time) { time_ = time; }
time::RealTimeClock *get_time() const { return time_; }
@@ -78,7 +78,7 @@ class Sun {
internal::GeoLocation location_;
};
class SunTrigger : public Trigger<>, public PollingComponent, public Parented<Sun> {
class SunTrigger final : public Trigger<>, public PollingComponent, public Parented<Sun> {
public:
SunTrigger() : PollingComponent(60000) {}
@@ -109,7 +109,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parented<Su
double elevation_;
};
template<typename... Ts> class SunCondition : public Condition<Ts...>, public Parented<Sun> {
template<typename... Ts> class SunCondition final : public Condition<Ts...>, public Parented<Sun> {
public:
TEMPLATABLE_VALUE(double, elevation);
void set_above(bool above) { above_ = above; }
@@ -8,7 +8,7 @@
namespace esphome::sun {
class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
class SunTextSensor final : public text_sensor::TextSensor, public PollingComponent {
public:
void set_parent(Sun *parent) { parent_ = parent; }
void set_elevation(double elevation) { elevation_ = elevation; }
+1 -1
View File
@@ -15,7 +15,7 @@
namespace esphome::sun_gtil2 {
class SunGTIL2 : public Component, public uart::UARTDevice {
class SunGTIL2 final : public Component, public uart::UARTDevice {
public:
float get_setup_priority() const override { return setup_priority::LATE; }
void setup() override;
+9 -9
View File
@@ -6,7 +6,7 @@
namespace esphome::switch_ {
template<typename... Ts> class TurnOnAction : public Action<Ts...> {
template<typename... Ts> class TurnOnAction final : public Action<Ts...> {
public:
explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {}
@@ -16,7 +16,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
Switch *switch_;
};
template<typename... Ts> class TurnOffAction : public Action<Ts...> {
template<typename... Ts> class TurnOffAction final : public Action<Ts...> {
public:
explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {}
@@ -26,7 +26,7 @@ template<typename... Ts> class TurnOffAction : public Action<Ts...> {
Switch *switch_;
};
template<typename... Ts> class ToggleAction : public Action<Ts...> {
template<typename... Ts> class ToggleAction final : public Action<Ts...> {
public:
explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {}
@@ -36,7 +36,7 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
Switch *switch_;
};
template<typename... Ts> class ControlAction : public Action<Ts...> {
template<typename... Ts> class ControlAction final : public Action<Ts...> {
public:
explicit ControlAction(Switch *a_switch) : switch_(a_switch) {}
@@ -53,7 +53,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
Switch *switch_;
};
template<typename... Ts> class SwitchCondition : public Condition<Ts...> {
template<typename... Ts> class SwitchCondition final : public Condition<Ts...> {
public:
SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
@@ -63,14 +63,14 @@ template<typename... Ts> class SwitchCondition : public Condition<Ts...> {
bool state_;
};
class SwitchStateTrigger : public Trigger<bool> {
class SwitchStateTrigger final : public Trigger<bool> {
public:
SwitchStateTrigger(Switch *a_switch) {
a_switch->add_on_state_callback([this](bool state) { this->trigger(state); });
}
};
class SwitchTurnOnTrigger : public Trigger<> {
class SwitchTurnOnTrigger final : public Trigger<> {
public:
SwitchTurnOnTrigger(Switch *a_switch) {
a_switch->add_on_state_callback([this](bool state) {
@@ -81,7 +81,7 @@ class SwitchTurnOnTrigger : public Trigger<> {
}
};
class SwitchTurnOffTrigger : public Trigger<> {
class SwitchTurnOffTrigger final : public Trigger<> {
public:
SwitchTurnOffTrigger(Switch *a_switch) {
a_switch->add_on_state_callback([this](bool state) {
@@ -92,7 +92,7 @@ class SwitchTurnOffTrigger : public Trigger<> {
}
};
template<typename... Ts> class SwitchPublishAction : public Action<Ts...> {
template<typename... Ts> class SwitchPublishAction final : public Action<Ts...> {
public:
SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {}
TEMPLATABLE_VALUE(bool, state)
@@ -6,7 +6,7 @@
namespace esphome::switch_ {
class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component {
class SwitchBinarySensor final : public binary_sensor::BinarySensor, public Component {
public:
void set_source(Switch *source) { source_ = source; }
void setup() override;
+6 -6
View File
@@ -6,12 +6,12 @@
namespace esphome::sx126x {
template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class RunImageCalAction final : public Action<Ts...>, public Parented<SX126x> {
public:
void play(const Ts &...x) override { this->parent_->run_image_cal(); }
};
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX126x> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
@@ -43,23 +43,23 @@ template<typename... Ts> class SendPacketAction : public Action<Ts...>, public P
} data_;
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class SetModeTxAction final : public Action<Ts...>, public Parented<SX126x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_tx(); }
};
template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class SetModeRxAction final : public Action<Ts...>, public Parented<SX126x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_rx(); }
};
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class SetModeSleepAction final : public Action<Ts...>, public Parented<SX126x> {
public:
TEMPLATABLE_VALUE(bool, cold)
void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {
template<typename... Ts> class SetModeStandbyAction final : public Action<Ts...>, public Parented<SX126x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); }
};
@@ -7,7 +7,7 @@
namespace esphome::sx126x {
class SX126xTransport : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
class SX126xTransport final : public packet_transport::PacketTransport, public Parented<SX126x>, public SX126xListener {
public:
void setup() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
+3 -3
View File
@@ -53,9 +53,9 @@ class SX126xListener {
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX126x : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
class SX126x final : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
size_t get_max_packet_size();
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+6 -6
View File
@@ -6,12 +6,12 @@
namespace esphome::sx127x {
template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class RunImageCalAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void play(const Ts &...x) override { this->parent_->run_image_cal(); }
};
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
@@ -43,22 +43,22 @@ template<typename... Ts> class SendPacketAction : public Action<Ts...>, public P
} data_;
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class SetModeTxAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_tx(); }
};
template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class SetModeRxAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_rx(); }
};
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class SetModeSleepAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_sleep(); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX127x> {
template<typename... Ts> class SetModeStandbyAction final : public Action<Ts...>, public Parented<SX127x> {
public:
void play(const Ts &...x) override { this->parent_->set_mode_standby(); }
};
@@ -7,7 +7,7 @@
namespace esphome::sx127x {
class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener {
class SX127xTransport final : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener {
public:
void setup() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
+3 -3
View File
@@ -41,9 +41,9 @@ class SX127xListener {
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX127x : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
class SX127x final : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
size_t get_max_packet_size();
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
@@ -5,7 +5,7 @@
namespace esphome::sx1509 {
class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor {
class SX1509BinarySensor final : public sx1509::SX1509Processor, public binary_sensor::BinarySensor {
public:
void set_row_col(uint8_t row, uint8_t col) { this->key_ = (1 << (col + 8)) | (1 << row); }
void process(uint16_t data) override { this->publish_state(static_cast<bool>(data == key_)); }
@@ -7,7 +7,7 @@ namespace esphome::sx1509 {
class SX1509Component;
class SX1509FloatOutputChannel : public output::FloatOutput, public Component {
class SX1509FloatOutputChannel final : public output::FloatOutput, public Component {
public:
void set_parent(SX1509Component *parent) { this->parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
+5 -5
View File
@@ -28,12 +28,12 @@ class SX1509Processor {
virtual void process(uint16_t data){};
};
class SX1509KeyTrigger : public Trigger<uint8_t> {};
class SX1509KeyTrigger final : public Trigger<uint8_t> {};
class SX1509Component : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16>,
public key_provider::KeyProvider {
class SX1509Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16>,
public key_provider::KeyProvider {
public:
SX1509Component() = default;
+1 -1
View File
@@ -6,7 +6,7 @@ namespace esphome::sx1509 {
class SX1509Component;
class SX1509GPIOPin : public GPIOPin {
class SX1509GPIOPin final : public GPIOPin {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
@@ -6,7 +6,7 @@
namespace esphome::sy6970 {
template<uint8_t REG, uint8_t SHIFT, uint8_t MASK, uint8_t TRUE_VALUE>
class StatusBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
class StatusBinarySensor final : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t value = (data.registers[REG] >> SHIFT) & MASK;
@@ -24,7 +24,7 @@ class InverseStatusBinarySensor : public SY6970Listener, public binary_sensor::B
};
// Custom binary sensor for charging (true when pre-charge or fast charge)
class SY6970ChargingBinarySensor : public SY6970Listener, public binary_sensor::BinarySensor {
class SY6970ChargingBinarySensor final : public SY6970Listener, public binary_sensor::BinarySensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t chrg_stat = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
@@ -34,7 +34,7 @@ using SY6970SystemVoltageSensor = VoltageSensor<SY6970_REG_VINDPM_STATUS, 0x7F,
using SY6970ChargeCurrentSensor = CurrentSensor<SY6970_REG_CHARGE_CURRENT_MONITOR, 0x7F, 0, CHG_CURRENT_STEP_MA>;
// Precharge current sensor needs special handling (bit shift)
class SY6970PrechargeCurrentSensor : public SY6970Listener, public sensor::Sensor {
class SY6970PrechargeCurrentSensor final : public SY6970Listener, public sensor::Sensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t iprechg = (data.registers[SY6970_REG_PRECHARGE_CURRENT] >> 4) & 0x0F;
+1 -1
View File
@@ -73,7 +73,7 @@ class SY6970Listener {
virtual void on_data(const SY6970Data &data) = 0;
};
class SY6970Component : public PollingComponent, public i2c::I2CDevice {
class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
@@ -6,7 +6,7 @@
namespace esphome::sy6970 {
// Bus status text sensor
class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
class SY6970BusStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 5) & 0x07;
@@ -40,7 +40,7 @@ class SY6970BusStatusTextSensor : public SY6970Listener, public text_sensor::Tex
};
// Charge status text sensor
class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
class SY6970ChargeStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = (data.registers[SY6970_REG_STATUS] >> 3) & 0x03;
@@ -66,7 +66,7 @@ class SY6970ChargeStatusTextSensor : public SY6970Listener, public text_sensor::
};
// NTC status text sensor
class SY6970NtcStatusTextSensor : public SY6970Listener, public text_sensor::TextSensor {
class SY6970NtcStatusTextSensor final : public SY6970Listener, public text_sensor::TextSensor {
public:
void on_data(const SY6970Data &data) override {
uint8_t status = data.registers[SY6970_REG_FAULT] & 0x07;
+1 -1
View File
@@ -7,7 +7,7 @@
#ifdef USE_NETWORK
namespace esphome::syslog {
class Syslog : public Component, public Parented<udp::UDPComponent> {
class Syslog final : public Component, public Parented<udp::UDPComponent> {
public:
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
void setup() override;
+1 -1
View File
@@ -19,7 +19,7 @@ enum class T6615Command : uint8_t {
SET_ELEVATION,
};
class T6615Component : public PollingComponent, public uart::UARTDevice {
class T6615Component final : public PollingComponent, public uart::UARTDevice {
public:
void loop() override;
void update() override;
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::tc74 {
class TC74Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
class TC74Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
/// Setup the sensor and check connection.
void setup() override;
+2 -2
View File
@@ -8,7 +8,7 @@ namespace esphome::tca9548a {
static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00;
class TCA9548AComponent;
class TCA9548AChannel : public i2c::I2CBus {
class TCA9548AChannel final : public i2c::I2CBus {
public:
void set_channel(uint8_t channel) { channel_ = channel; }
void set_parent(TCA9548AComponent *parent) { parent_ = parent; }
@@ -21,7 +21,7 @@ class TCA9548AChannel : public i2c::I2CBus {
TCA9548AComponent *parent_;
};
class TCA9548AComponent : public Component, public i2c::I2CDevice {
class TCA9548AComponent final : public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
+4 -4
View File
@@ -7,9 +7,9 @@
namespace esphome::tca9555 {
class TCA9555Component : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
class TCA9555Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
public:
TCA9555Component() = default;
@@ -47,7 +47,7 @@ class TCA9555Component : public Component,
};
/// Helper class to expose a TCA9555 pin as an internal input GPIO pin.
class TCA9555GPIOPin : public GPIOPin, public Parented<TCA9555Component> {
class TCA9555GPIOPin final : public GPIOPin, public Parented<TCA9555Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
+1 -1
View File
@@ -8,7 +8,7 @@ namespace esphome::tcl112 {
const float TCL112_TEMP_MAX = 31.0;
const float TCL112_TEMP_MIN = 16.0;
class Tcl112Climate : public climate_ir::ClimateIR {
class Tcl112Climate final : public climate_ir::ClimateIR {
public:
Tcl112Climate()
: climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f, true, true,
+1 -1
View File
@@ -35,7 +35,7 @@ enum TCS34725Gain {
TCS34725_GAIN_60X = 0x03,
};
class TCS34725Component : public PollingComponent, public i2c::I2CDevice {
class TCS34725Component final : public PollingComponent, public i2c::I2CDevice {
public:
void set_integration_time(TCS34725IntegrationTime integration_time);
void set_gain(TCS34725Gain gain);
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::tee501 {
/// This class implements support for the tee501 of temperature i2c sensors.
class TEE501Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
class TEE501Component final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
@@ -4,7 +4,7 @@
namespace esphome::teleinfo {
class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component {
class TeleInfoSensor final : public TeleInfoListener, public sensor::Sensor, public Component {
public:
TeleInfoSensor(const char *tag);
void publish_val(const std::string &val) override;
+1 -1
View File
@@ -20,7 +20,7 @@ class TeleInfoListener {
std::string tag;
virtual void publish_val(const std::string &val){};
};
class TeleInfo : public PollingComponent, public uart::UARTDevice {
class TeleInfo final : public PollingComponent, public uart::UARTDevice {
public:
TeleInfo(bool historical_mode);
void register_teleinfo_listener(TeleInfoListener *listener);
@@ -3,7 +3,7 @@
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::teleinfo {
class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component {
class TeleInfoTextSensor final : public TeleInfoListener, public text_sensor::TextSensor, public Component {
public:
TeleInfoTextSensor(const char *tag);
void publish_val(const std::string &val) override;
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::tem3200 {
/// This class implements support for the tem3200 pressure and temperature i2c sensors.
class TEM3200Component : public PollingComponent, public i2c::I2CDevice {
class TEM3200Component final : public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_raw_pressure_sensor(sensor::Sensor *raw_pressure_sensor) {
+1 -1
View File
@@ -15,7 +15,7 @@ puremagic==2.2.0
ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
esphome-glyphsets==0.2.0
pillow==12.3.0
pillow==12.2.0
resvg-py==0.3.3
freetype-py==2.5.1
jinja2==3.1.6
@@ -1,84 +0,0 @@
"""Tests for modbus_server configuration validation."""
import pytest
from esphome import config_validation as cv
from esphome.components.modbus_server import (
SERVER_SENSOR_VALUE_TYPE,
_validate_no_overlapping_registers,
_validate_register_ranges,
)
from esphome.components.modbus_server.const import CONF_REGISTERS, CONF_VALUE_TYPE
from esphome.const import CONF_ADDRESS
def _config(registers: list[tuple[int, str]]) -> dict:
return {
CONF_REGISTERS: [
{CONF_ADDRESS: address, CONF_VALUE_TYPE: value_type}
for address, value_type in registers
]
}
def test_non_overlapping_registers_pass() -> None:
# Values that tile the address space without gaps or overlaps are accepted.
config = _config([(0x00, "U_WORD"), (0x01, "U_DWORD"), (0x03, "U_WORD")])
assert _validate_no_overlapping_registers(config) is config
def test_registers_with_gaps_pass() -> None:
config = _config([(0x00, "U_WORD"), (0x05, "U_QWORD"), (0x20, "U_WORD")])
assert _validate_no_overlapping_registers(config) is config
def test_no_registers_pass() -> None:
assert _validate_no_overlapping_registers({}) == {}
def test_duplicate_address_rejected() -> None:
config = _config([(0x10, "U_WORD"), (0x10, "U_WORD")])
with pytest.raises(cv.Invalid, match="overlaps"):
_validate_no_overlapping_registers(config)
def test_multi_register_value_overlapping_neighbour_rejected() -> None:
# U_DWORD at 0x10 occupies 0x10 and 0x11; a U_WORD at 0x11 collides with its low word.
config = _config([(0x10, "U_DWORD"), (0x11, "U_WORD")])
with pytest.raises(cv.Invalid, match="overlaps"):
_validate_no_overlapping_registers(config)
def test_overlap_detected_regardless_of_order() -> None:
# The U_DWORD at 0x10 covers 0x10-0x11 and overlaps the U_WORD at 0x11 even when declared after it.
config = _config([(0x11, "U_WORD"), (0x10, "U_DWORD")])
with pytest.raises(cv.Invalid, match="overlaps"):
_validate_no_overlapping_registers(config)
def test_register_span_within_address_space_pass() -> None:
# A value whose span ends exactly at 0xFFFF is fine (U_QWORD at 0xFFFC covers 0xFFFC-0xFFFF).
config = _config([(0xFFFF, "U_WORD"), (0xFFFC, "U_QWORD")])
assert _validate_register_ranges(config) is config
def test_register_span_past_end_rejected() -> None:
# U_QWORD at 0xFFFE would need 0xFFFE-0x10001, running off the 16-bit address space.
config = _config([(0xFFFE, "U_QWORD")])
with pytest.raises(cv.Invalid, match="past the end"):
_validate_register_ranges(config)
def test_multi_register_value_at_last_address_rejected() -> None:
# A U_DWORD at 0xFFFF needs a second register at 0x10000, which does not exist.
config = _config([(0xFFFF, "U_DWORD")])
with pytest.raises(cv.Invalid, match="past the end"):
_validate_register_ranges(config)
def test_raw_value_type_rejected() -> None:
# RAW has no numeric encoding, so it is not offered as a server register type.
validator = cv.enum(SERVER_SENSOR_VALUE_TYPE)
with pytest.raises(cv.Invalid):
validator("RAW")
assert validator("U_WORD") == "U_WORD"
@@ -18,7 +18,6 @@ modbus_server:
registers:
- address: 0x9
value_type: S_DWORD
allow_partial_read: true
read_lambda: |-
return 31;
write_lambda: |-
@@ -121,165 +121,4 @@ TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) {
EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure
}
// --- on_modbus_read_registers --------------------------------------------------
TEST(ModbusServerRead, SingleWordSucceeds) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_WORD, 1);
reg.read_lambda = []() -> int64_t { return 0x1234; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
}
TEST(ModbusServerRead, DwordReturnsTwoWordsHighFirst) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2);
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0x1234);
EXPECT_EQ(out[1], 0x5678);
}
// Starting inside a multi-register value is rejected with ILLEGAL_DATA_ADDRESS -- not masked by the courtesy
// default -- and the read_lambda is never invoked.
TEST(ModbusServerRead, StartInsideValueRejected) {
ModbusServer server;
bool read_called = false;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2); // occupies 0x0010 and 0x0011
reg.read_lambda = [&read_called]() -> int64_t {
read_called = true;
return 0;
};
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out); // the second cell of the DWORD
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
EXPECT_FALSE(read_called);
}
// A read that stops short of a value's end clips it -> ILLEGAL_DATA_ADDRESS, and the read_lambda is not invoked.
TEST(ModbusServerRead, ClippedTailRejected) {
ModbusServer server;
bool read_called = false;
ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2);
reg.read_lambda = [&read_called]() -> int64_t {
read_called = true;
return 0;
};
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
EXPECT_FALSE(read_called);
}
// A write-only register (no read_lambda) is not readable -> ILLEGAL_DATA_ADDRESS, not a courtesy default.
TEST(ModbusServerRead, WriteOnlyRegisterRejected) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_WORD, 1); // no read_lambda set
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// An unregistered address with courtesy enabled returns the default value for each cell.
TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
ModbusServer server;
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0xABCD);
EXPECT_EQ(out[1], 0xABCD);
}
// An unregistered address with courtesy disabled is rejected.
TEST(ModbusServerRead, UnregisteredRejectedWithoutCourtesy) {
ModbusServer server;
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// --- partial reads (opt-in) ----------------------------------------------------
// With allow_partial_read, reading only the first register of a DWORD returns its high word.
TEST(ModbusServerRead, PartialReadHighWord) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0010, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
}
// With allow_partial_read, starting at the interior cell returns the low word.
TEST(ModbusServerRead, PartialReadLowWordFromInterior) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x5678);
}
// Slicing is in wire order, so a reversed value type partials correctly: U_DWORD_R emits the low word
// first, so 0x0010 holds 0x5678 and 0x0011 holds 0x1234.
TEST(ModbusServerRead, PartialReadReversedType) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD_R, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues first;
ASSERT_FALSE(server.on_modbus_read_registers(0x0010, 1, first).has_value());
ASSERT_EQ(first.size(), 1u);
EXPECT_EQ(first[0], 0x5678);
RegisterValues second;
ASSERT_FALSE(server.on_modbus_read_registers(0x0011, 1, second).has_value());
ASSERT_EQ(second.size(), 1u);
EXPECT_EQ(second[0], 0x1234);
}
} // namespace esphome::modbus_server