mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 16:30:22 +00:00
[modbus_controller] Add continuous polling option (#18080)
This commit is contained in:
@@ -11,7 +11,14 @@ from esphome.components.modbus.helpers import (
|
||||
EntityType,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CONTINUOUS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_NAME,
|
||||
CONF_OFFSET,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_helpers import logging
|
||||
import esphome.final_validate as fv
|
||||
@@ -125,6 +132,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
|
||||
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
|
||||
**modbus.command_options_schema(direction="read"),
|
||||
cv.Optional(
|
||||
CONF_SERVER_REGISTERS,
|
||||
): cv.invalid(
|
||||
@@ -234,6 +242,35 @@ def migrate_custom_command(config: ConfigType) -> None:
|
||||
del config[CONF_CUSTOM_COMMAND]
|
||||
|
||||
|
||||
def _reject_continuous_write_custom_pdu(config: ConfigType) -> None:
|
||||
"""Final-validate: a custom_pdu whose function code writes (e.g. 0x17 read/write-multiple) cannot be
|
||||
polled continuously - the hub ignores continuous for mutating codes and would warn on every update
|
||||
while that range silently does not stream. Reject the combination instead. Runs after
|
||||
migrate_custom_command, so it sees custom_pdu whether written directly or migrated from
|
||||
custom_command."""
|
||||
pdu = config.get(CONF_CUSTOM_PDU)
|
||||
if pdu is None or not modbus.is_function_code_write(pdu[0]):
|
||||
return
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1]
|
||||
controller = fconf.get_config_for_path(path)
|
||||
if controller.get(CONF_CONTINUOUS) is True:
|
||||
raise cv.Invalid(
|
||||
f"a '{CONF_CUSTOM_PDU}' with a write function code (0x{pdu[0] & 0x7F:02X}) can't be polled "
|
||||
f"continuously: the hub ignores 'continuous' for mutating codes. Remove 'continuous: true' "
|
||||
f"from the '{controller[CONF_ID]}' modbus_controller, or use a read function code.",
|
||||
[CONF_CUSTOM_PDU],
|
||||
)
|
||||
|
||||
|
||||
def validate_custom_pdu_item(config: ConfigType) -> None:
|
||||
"""Final-validate for the read platforms that accept custom_pdu (sensor, binary_sensor,
|
||||
text_sensor): migrate the deprecated custom_command, then reject a write-coded custom_pdu under a
|
||||
continuously-polling controller."""
|
||||
migrate_custom_command(config)
|
||||
_reject_continuous_write_custom_pdu(config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
modbus.final_validate_modbus_device("modbus_controller", role="client")(config)
|
||||
|
||||
@@ -314,6 +351,11 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
|
||||
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
|
||||
cg.add(
|
||||
var.set_read_options(
|
||||
modbus.command_options_expression(config, direction="read")
|
||||
)
|
||||
)
|
||||
await register_modbus_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -167,6 +167,7 @@ void ModbusController::queue_command(ModbusCommandItem command) {
|
||||
this->one_shot_command_items_.push_back(make_unique<ModbusCommandItem>(std::move(command)));
|
||||
// A refused frame gets no terminal callback (see the hub contract), so reclaim the item here.
|
||||
auto &item = this->one_shot_command_items_.back();
|
||||
// We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling.
|
||||
if (!item->send()) {
|
||||
// The caller (e.g. a write entity) has usually already published optimistically - surface the loss.
|
||||
ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast<uint8_t>(item->register_type()),
|
||||
@@ -203,7 +204,9 @@ void ModbusController::update() {
|
||||
ESP_LOGV(TAG, "Module offline - retrying");
|
||||
this->cmd_non_responses_ = 0; // allow the probe through can_send()
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
if (!cmd.send()) {
|
||||
// Probes carry the read-side options too, so a recovering device resumes streaming on the
|
||||
// probe itself rather than waiting for the next update_interval.
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
}
|
||||
@@ -217,8 +220,9 @@ void ModbusController::update() {
|
||||
if (this->can_send()) {
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address());
|
||||
// read_options_ carries the controller's continuous flag (the offline probe above sends it too).
|
||||
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
|
||||
if (!cmd.send()) {
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address());
|
||||
}
|
||||
}
|
||||
@@ -496,16 +500,18 @@ ModbusCommandItem ModbusCommandItem::create_custom_command(
|
||||
return cmd;
|
||||
}
|
||||
|
||||
bool ModbusCommandItem::send() {
|
||||
bool ModbusCommandItem::send(modbus::CommandOptions options) {
|
||||
// Options pass straight through to the hub
|
||||
bool accepted;
|
||||
if (this->custom_pdu_ != nullptr) {
|
||||
// Custom polling command: send the sensor's ready-made PDU (function code + data, no address byte)
|
||||
// to this controller's own device address; the hub prepends the address and appends the CRC.
|
||||
accepted = modbus::ModbusClientDevice::queue_pdu(std::span<const uint8_t>(*this->custom_pdu_));
|
||||
accepted = modbus::ModbusClientDevice::queue_pdu(std::span<const uint8_t>(*this->custom_pdu_), options);
|
||||
} else if (this->function_code_ != FunctionCode::CUSTOM) {
|
||||
accepted = this->queue_pdu(modbus::helpers::create_client_pdu(
|
||||
this->function_code_, this->start_address_, this->register_count_,
|
||||
this->payload.empty() ? nullptr : this->payload.data(), this->payload.size()));
|
||||
this->function_code_, this->start_address_, this->register_count_,
|
||||
this->payload.empty() ? nullptr : this->payload.data(), this->payload.size()),
|
||||
options);
|
||||
} else {
|
||||
// Factory custom command: payload holds a complete raw frame (address + PDU). Send the PDU to the
|
||||
// frame's own address (which may differ from this controller's); the hub appends the CRC and routes
|
||||
@@ -515,7 +521,7 @@ bool ModbusCommandItem::send() {
|
||||
ESP_LOGW(TAG, "Empty custom command frame, not sent");
|
||||
accepted = false;
|
||||
} else {
|
||||
accepted = this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
|
||||
accepted = this->parent_->queue_pdu(frame[0], frame.subspan(1), this, options);
|
||||
}
|
||||
}
|
||||
// The on_command_sent trigger fires from on_sent() when the frame actually reaches the wire.
|
||||
|
||||
@@ -284,7 +284,9 @@ class ModbusCommandItem : public modbus::ModbusClientDevice {
|
||||
/// Queue this command's frame on the hub. Returns false when refused, in which case no callback ever comes.
|
||||
/// The item is the hub device, so it must stay alive until its terminal callback; a destroyed item's
|
||||
/// pending frame is silently retired.
|
||||
bool send();
|
||||
/// Options pass straight through to the hub; the polling path passes the controller's read-side
|
||||
/// options so reads re-queue after each success, one-shot commands keep the default.
|
||||
bool send(modbus::CommandOptions options = {});
|
||||
|
||||
/// factory methods
|
||||
/** Create modbus read command
|
||||
@@ -452,6 +454,10 @@ class ModbusController final : public PollingComponent {
|
||||
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
|
||||
/// get how many times a command will be (re)sent if no response is received
|
||||
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
|
||||
/// called by esphome generated code with the read-side command options applied to every poll
|
||||
void set_read_options(modbus::CommandOptions options) { this->read_options_ = options; }
|
||||
/// the read-side command options applied to every poll
|
||||
const modbus::CommandOptions &read_options() const { return this->read_options_; }
|
||||
|
||||
protected:
|
||||
/// parse sensormap_ and create range of sequential addresses
|
||||
@@ -497,6 +503,8 @@ class ModbusController final : public PollingComponent {
|
||||
uint16_t offline_skip_updates_{0};
|
||||
/// How many times we will retry a command if we get no response
|
||||
uint8_t max_cmd_retries_{4};
|
||||
/// read-side command options applied to every poll
|
||||
modbus::CommandOptions read_options_{};
|
||||
/// Command sent callback
|
||||
CallbackManager<void(int, int)> command_sent_callback_{};
|
||||
/// Server online callback
|
||||
|
||||
@@ -18,9 +18,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
)
|
||||
from ..const import (
|
||||
CONF_BITMASK,
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_number,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -8,9 +8,9 @@ from .. import (
|
||||
ModbusItemBaseSchema,
|
||||
SensorItem,
|
||||
add_modbus_base_properties,
|
||||
migrate_custom_command,
|
||||
modbus_calc_properties,
|
||||
modbus_controller_ns,
|
||||
validate_custom_pdu_item,
|
||||
validate_modbus_register,
|
||||
)
|
||||
from ..const import (
|
||||
@@ -55,7 +55,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_modbus_register,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = migrate_custom_command
|
||||
FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
Reference in New Issue
Block a user