diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index b927faf9a7..f919cb0678 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -47,11 +47,16 @@ MODBUS_FUNCTION_CODE = { ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") -MODBUS_REGISTER_TYPE = { + +MODBUS_WRITE_REGISTER_TYPE = { "custom": ModbusRegisterType.CUSTOM, "coil": ModbusRegisterType.COIL, - "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "read": ModbusRegisterType.READ, } diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index a26d05a18b..1bf989ce8b 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output - from esphome.const import ( CONF_ADDRESS, CONF_ID, @@ -11,13 +10,14 @@ from esphome.const import ( from .. import ( modbus_controller_ns, modbus_calc_properties, - validate_modbus_register, ModbusItemBaseSchema, SensorItem, + SENSOR_VALUE_TYPE, ) from ..const import ( CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, @@ -27,45 +27,83 @@ DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] -ModbusOutput = modbus_controller_ns.class_( - "ModbusOutput", cg.Component, output.FloatOutput, SensorItem +ModbusFloatOutput = modbus_controller_ns.class_( + "ModbusFloatOutput", cg.Component, output.FloatOutput, SensorItem +) +ModbusBinaryOutput = modbus_controller_ns.class_( + "ModbusBinaryOutput", cg.Component, output.BinaryOutput, SensorItem ) -CONFIG_SCHEMA = cv.All( - output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( - { - cv.GenerateID(): cv.declare_id(ModbusOutput), - 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, - } - ), - validate_modbus_register, + +CONFIG_SCHEMA = cv.typed_schema( + { + "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + 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.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, + } + ), + }, + lower=True, + key=CONF_REGISTER_TYPE, + default_type="holding", ) async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) - var = cg.new_Pvariable( - config[CONF_ID], - config[CONF_ADDRESS], - byte_offset, - config[CONF_VALUE_TYPE], - reg_count, - ) + # Binary Output + if config[CONF_REGISTER_TYPE] == "coil": + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + ) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusBinaryOutput.operator("ptr"), "item"), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), + ], + return_type=cg.optional.template(bool), + ) + # Float Output + else: + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + config[CONF_VALUE_TYPE], + reg_count, + ) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusFloatOutput.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) await output.register_output(var, config) - cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) cg.add(var.set_parent(parent)) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_WRITE_LAMBDA], - [ - (ModbusOutput.operator("ptr"), "item"), - (cg.float_, "x"), - (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), - ], - return_type=cg.optional.template(float), - ) cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index 4c2e5775b9..b647312f52 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -1,18 +1,17 @@ #include #include "modbus_output.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus_controller { static const char *const TAG = "modbus_controller.output"; -void ModbusOutput::setup() {} - /** Write a value to the device * */ -void ModbusOutput::write_state(float value) { +void ModbusFloatOutput::write_state(float value) { std::vector data; auto original_value = value; // Is there are lambda configured? @@ -39,7 +38,6 @@ void ModbusOutput::write_state(float value) { ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", this->start_address, this->register_count, value, original_value); - // Create and send the write command // Create and send the write command ModbusCommandItem write_cmd; if (this->register_count == 1 && !this->use_write_multiple_) { @@ -51,11 +49,62 @@ void ModbusOutput::write_state(float value) { parent_->queue_command(write_cmd); } -void ModbusOutput::dump_config() { +void ModbusFloatOutput::dump_config() { ESP_LOGCONFIG(TAG, "Modbus Float Output:"); LOG_FLOAT_OUTPUT(this); - ESP_LOGCONFIG(TAG, "Modbus device start address=0x%X register count=%d value type=%hhu", this->start_address, - this->register_count, this->sensor_value_type); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); +} + +// ModbusBinaryOutput +void ModbusBinaryOutput::write_state(bool state) { + // This will be called every time the user requests a state change. + ModbusCommandItem cmd; + std::vector data; + + // Is there are lambda configured? + if (this->write_transform_func_.has_value()) { + // data is passed by reference + // the lambda can fill the empty vector directly + // in that case the return value is ignored + auto val = (*this->write_transform_func_)(this, state, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + state = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus binary output write raw: %s", format_hex_pretty(data).c_str()); + cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(cmd.register_type, this->start_address, data); + }); + } else { + ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state), + (int) this->register_type, this->start_address, this->offset); + + // offset for coil and discrete inputs is the coil/register number not bytes + if (this->use_write_multiple_) { + std::vector states{state}; + cmd = ModbusCommandItem::create_write_multiple_coils(parent_, this->start_address + this->offset, states); + } else { + cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); + } + } + this->parent_->queue_command(cmd); +} + +void ModbusBinaryOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Binary Output:"); + LOG_BINARY_OUTPUT(this); + ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address); + ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count); + ESP_LOGCONFIG(TAG, " Value type: %d", static_cast(this->sensor_value_type)); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 78d3474ad6..6237805d24 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -7,11 +7,9 @@ namespace esphome { namespace modbus_controller { -using value_to_data_t = std::function(float); - -class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { +class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { public: - ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) + ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) : output::FloatOutput(), Component() { this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; @@ -23,7 +21,6 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor this->start_address += offset; this->offset = 0; } - void setup() override; void dump_config() override; void set_parent(ModbusController *parent) { this->parent_ = parent; } @@ -31,7 +28,7 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor // Do nothing void parse_and_publish(const std::vector &data) override{}; - using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; + using write_transform_func_t = std::function(ModbusFloatOutput *, float, std::vector &)>; void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } @@ -44,5 +41,35 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor bool use_write_multiple_; }; +class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem { + public: + ModbusBinaryOutput(uint16_t start_address, uint8_t offset) : output::BinaryOutput(), Component() { + this->register_type = ModbusRegisterType::COIL; + this->start_address = start_address; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = 0; + this->register_count = 1; + this->start_address += offset; + this->offset = 0; + } + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + using write_transform_func_t = std::function(ModbusBinaryOutput *, bool, std::vector &)>; + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + + protected: + void write_state(bool state) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + bool use_write_multiple_; +}; + } // namespace modbus_controller } // namespace esphome