From 17a37b1de929ccdd9485269a934da3801380da8c Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Fri, 26 Nov 2021 00:48:52 +0100 Subject: [PATCH] Modbus_controller: Add custom command. (#2680) --- .../components/modbus_controller/__init__.py | 111 ++++++++++++++++-- .../binary_sensor/__init__.py | 49 +++----- esphome/components/modbus_controller/const.py | 1 + .../modbus_controller/modbus_controller.cpp | 59 +++++++--- .../modbus_controller/modbus_controller.h | 33 ++++-- .../modbus_controller/number/__init__.py | 64 ++++------ .../modbus_controller/output/__init__.py | 33 ++---- .../modbus_controller/sensor/__init__.py | 55 +++------ .../modbus_controller/sensor/modbus_sensor.h | 1 + .../modbus_controller/switch/__init__.py | 59 +++++----- .../switch/modbus_switch.cpp | 56 ++++++--- .../modbus_controller/switch/modbus_switch.h | 5 + .../modbus_controller/text_sensor/__init__.py | 50 +++----- 13 files changed, 315 insertions(+), 261 deletions(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 8499cec561..825b91280e 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -1,10 +1,20 @@ +import binascii import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import modbus -from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_LAMBDA, CONF_OFFSET from esphome.cpp_helpers import logging from .const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, + CONF_CUSTOM_COMMAND, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, ) CODEOWNERS = ["@martgras"] @@ -37,6 +47,7 @@ MODBUS_FUNCTION_CODE = { ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") MODBUS_REGISTER_TYPE = { + "custom": ModbusRegisterType.CUSTOM, "coil": ModbusRegisterType.COIL, "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "holding": ModbusRegisterType.HOLDING, @@ -95,6 +106,96 @@ CONFIG_SCHEMA = cv.All( ) +ModbusItemBaseSchema = cv.Schema( + { + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Optional(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_COMMAND): cv.ensure_list(cv.hex_uint8_t), + cv.Exclusive( + CONF_OFFSET, + "offset", + f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together", + ): cv.positive_int, + cv.Exclusive( + CONF_BYTE_OFFSET, + "offset", + f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together", + ): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + }, +) + + +def validate_modbus_register(config): + if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config: + raise cv.Invalid( + f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + if CONF_CUSTOM_COMMAND in config and CONF_REGISTER_TYPE in config: + raise cv.Invalid( + f"can't use '{CONF_REGISTER_TYPE}:' together with '{CONF_CUSTOM_COMMAND}:'", + ) + + if CONF_CUSTOM_COMMAND not in config and CONF_REGISTER_TYPE not in config: + raise cv.Invalid( + f" {CONF_REGISTER_TYPE} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + return config + + +def modbus_calc_properties(config): + byte_offset = 0 + reg_count = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + if CONF_REGISTER_COUNT in config: + reg_count = config[CONF_REGISTER_COUNT] + if CONF_VALUE_TYPE in config: + value_type = config[CONF_VALUE_TYPE] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + if CONF_CUSTOM_COMMAND in config: + if CONF_ADDRESS not in config: + # generate a unique modbus address using the hash of the name + # CONF_NAME set even if only CONF_ID is used. + # a modbus register address is required to add the item to sensormap + value = config[CONF_NAME] + if isinstance(value, str): + value = value.encode() + config[CONF_ADDRESS] = binascii.crc_hqx(value, 0) + config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM + config[CONF_FORCE_NEW_RANGE] = True + return byte_offset, reg_count + + +async def add_modbus_base_properties( + var, config, sensor_type, lamdba_param_type=cg.float_, lamdba_return_type=float +): + if CONF_CUSTOM_COMMAND in config: + cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND])) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (sensor_type.operator("ptr"), "item"), + (lamdba_param_type, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(lamdba_return_type), + ) + cg.add(var.set_template(template_)) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_COMMAND_THROTTLE]) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) @@ -119,11 +220,3 @@ def function_code_to_register(function_code): "write_multiple_registers": ModbusRegisterType.HOLDING, } return FUNCTION_CODE_TYPE_MAP[function_code] - - -def find_by_value(dict, find_value): - for (key, value) in MODBUS_REGISTER_TYPE.items(): - print(find_value, value) - if find_value == value: - return key - return "not found" diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index d46ff71f2d..99d56fed67 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -2,16 +2,18 @@ from esphome.components import binary_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, @@ -27,30 +29,20 @@ ModbusBinarySensor = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.BINARY_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusBinarySensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, _ = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], @@ -65,17 +57,4 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusBinarySensor.operator("ptr"), "item"), - (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(bool), - ) - cg.add(var.set_template(template_)) + await add_modbus_base_properties(var, config, ModbusBinarySensor, cg.float_, bool) diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 3cd114e673..8d1676dd38 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -1,6 +1,7 @@ CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" +CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 70b5bf8eae..8b96c20691 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -28,7 +28,10 @@ bool ModbusController::send_next_command_() { command->register_address, command->register_count); command->send(); this->last_command_timestamp_ = millis(); - if (!command->on_data_func) { // No handler remove from queue directly after sending + // remove from queue if no handler is defined or command was sent too often + if (!command->on_data_func || command->send_countdown < 1) { + ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X countdown=%d removed from queue after send", + this->address_, command->register_address, command->send_countdown); command_queue_.pop_front(); } } @@ -69,24 +72,30 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ } } -void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, - const std::vector &data) { - ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); - +std::map::iterator ModbusController::find_register_(ModbusRegisterType register_type, + uint16_t start_address) { auto vec_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); }); if (vec_it == register_ranges_.end()) { - ESP_LOGE(TAG, "Handle incoming data : No matching range for sensor found - start_address : 0x%X", start_address); - return; - } - auto map_it = sensormap_.find(vec_it->first_sensorkey); - if (map_it == sensormap_.end()) { - ESP_LOGE(TAG, "Handle incoming data : No sensor found in at start_address : 0x%X (0x%llX)", start_address, - vec_it->first_sensorkey); - return; + ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address); + } else { + auto map_it = sensormap_.find(vec_it->first_sensorkey); + if (map_it == sensormap_.end()) { + ESP_LOGE(TAG, "No sensor found in at start_address : 0x%X (0x%llX)", start_address, vec_it->first_sensorkey); + } else { + return sensormap_.find(vec_it->first_sensorkey); + } } + // not found + return std::end(sensormap_); +} +void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); + + auto map_it = find_register_(register_type, start_address); // loop through all sensors with the same start address while (map_it != sensormap_.end() && map_it->second->start_address == start_address) { if (map_it->second->register_type == register_type) { @@ -116,9 +125,23 @@ void ModbusController::update_range_(RegisterRange &r) { ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type, r.skip_updates_counter); if (r.skip_updates_counter == 0) { - ModbusCommandItem command_item = - ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count); - queue_command(command_item); + // if a custom command is used the user supplied custom_data is only available in the SensorItem. + if (r.register_type == ModbusRegisterType::CUSTOM) { + auto it = this->find_register_(r.register_type, r.start_address); + if (it != sensormap_.end()) { + auto command_item = ModbusCommandItem::create_custom_command( + this, it->second->custom_data, + [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data); + }); + command_item.register_address = it->second->start_address; + command_item.register_count = it->second->register_count; + command_item.function_code = ModbusFunctionCode::CUSTOM; + queue_command(command_item); + } + } else { + queue_command(ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count)); + } r.skip_updates_counter = r.skip_updates; // reset counter to config value } else { r.skip_updates_counter--; @@ -422,6 +445,7 @@ bool ModbusCommandItem::send() { modbusdevice->send_raw(this->payload); } ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count); + send_countdown--; return true; } @@ -549,6 +573,9 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ ESP_LOGD(TAG, "FP32_R = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); result = raw_to_float.float_value; } break; + case SensorValueType::RAW: + result = NAN; + break; default: break; } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 222ebbd020..39c0d8026f 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -247,18 +247,11 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ class ModbusController; -struct SensorItem { - ModbusRegisterType register_type; - SensorValueType sensor_value_type; - uint16_t start_address; - uint32_t bitmask; - uint8_t offset; - uint8_t register_count; - uint8_t skip_updates; - bool force_new_range{false}; - +class SensorItem { + public: virtual void parse_and_publish(const std::vector &data) = 0; + void set_custom_data(const std::vector &data) { custom_data = data; } uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } size_t virtual get_register_size() const { if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) @@ -266,10 +259,22 @@ struct SensorItem { else return register_count * 2; } + + ModbusRegisterType register_type; + SensorValueType sensor_value_type; + uint16_t start_address; + uint32_t bitmask; + uint8_t offset; + uint8_t register_count; + uint8_t skip_updates; + std::vector custom_data{}; + bool force_new_range{false}; }; -struct ModbusCommandItem { +class ModbusCommandItem { + public: static const size_t MAX_PAYLOAD_BYTES = 240; + static const uint8_t MAX_SEND_REPEATS = 5; ModbusController *modbusdevice; uint16_t register_address; uint16_t register_count; @@ -279,7 +284,9 @@ struct ModbusCommandItem { on_data_func; std::vector payload = {}; bool send(); - + // wrong commands (esp. custom commands) can block the send queue + // limit the number of repeats + uint8_t send_countdown{MAX_SEND_REPEATS}; /// factory methods /** Create modbus read command * Function code 02-04 @@ -392,6 +399,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { protected: /// parse sensormap_ and create range of sequential addresses size_t create_register_ranges_(); + // find register in sensormap. Returns iterator with all registers having the same start address + std::map::iterator find_register_(ModbusRegisterType register_type, uint16_t start_address); /// submit the read command for the address range to the send queue void update_range_(RegisterRange &r); /// parse incoming modbus data diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 4de0ffbcea..3c5db9b9c8 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -4,29 +4,26 @@ from esphome.components import number from esphome.const import ( CONF_ADDRESS, CONF_ID, - CONF_LAMBDA, CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MULTIPLY, - CONF_OFFSET, CONF_STEP, ) from .. import ( + add_modbus_base_properties, modbus_controller_ns, - ModbusController, - SENSOR_VALUE_TYPE, + modbus_calc_properties, + ModbusItemBaseSchema, SensorItem, - TYPE_REGISTER_MAP, + SENSOR_VALUE_TYPE, ) - from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, + CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, - CONF_REGISTER_COUNT, CONF_SKIP_UPDATES, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, @@ -51,22 +48,21 @@ def validate_min_max(config): return config +def validate_modbus_number(config): + if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config: + raise cv.Invalid( + f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used" + ) + return config + + CONFIG_SCHEMA = cv.All( - number.NUMBER_SCHEMA.extend( + number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusNumber), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.GenerateID(): cv.declare_id(ModbusNumber), # 24 bits are the maximum value for fp32 before precison is lost # 0x00FFFFFF = 16777215 cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, @@ -74,22 +70,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STEP, default=1): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate_min_max, + validate_modbus_number, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] - value_type = config[CONF_VALUE_TYPE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] + byte_offset, reg_count = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_ADDRESS], @@ -115,20 +104,7 @@ async def to_code(config): cg.add(var.set_parent(parent)) cg.add(parent.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusNumber.operator("ptr"), "item"), - (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_template(template_)) + await add_modbus_base_properties(var, config, ModbusNumber) if CONF_WRITE_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_WRITE_LAMBDA], diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 4aca4db64f..eacd96579f 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -6,24 +6,21 @@ from esphome.const import ( CONF_ADDRESS, CONF_ID, CONF_MULTIPLY, - CONF_OFFSET, ) from .. import ( - SensorItem, modbus_controller_ns, - ModbusController, - TYPE_REGISTER_MAP, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, ) from ..const import ( - CONF_BYTE_OFFSET, CONF_MODBUS_CONTROLLER_ID, - CONF_REGISTER_COUNT, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) -from ..sensor import SENSOR_VALUE_TYPE DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] @@ -34,38 +31,24 @@ ModbusOutput = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - output.FLOAT_OUTPUT_SCHEMA.extend( + output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( { - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), cv.GenerateID(): cv.declare_id(ModbusOutput), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, } ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] - value_type = config[CONF_VALUE_TYPE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] + byte_offset, reg_count = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_ADDRESS], byte_offset, - value_type, + config[CONF_VALUE_TYPE], reg_count, ) await output.register_output(var, config) diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 82acfe120b..da7b8928b4 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -2,18 +2,19 @@ from esphome.components import sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE, - TYPE_REGISTER_MAP, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_COUNT, @@ -31,43 +32,30 @@ ModbusSensor = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - sensor.SENSOR_SCHEMA.extend( + sensor.SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusSensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, reg_count = modbus_calc_properties(config) value_type = config[CONF_VALUE_TYPE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], - config[CONF_VALUE_TYPE], + value_type, reg_count, config[CONF_SKIP_UPDATES], config[CONF_FORCE_NEW_RANGE], @@ -77,17 +65,4 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusSensor.operator("ptr"), "item"), - (cg.float_, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_template(template_)) + await add_modbus_base_properties(var, config, ModbusSensor) diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 4f48c2a4dd..37ea9d0dd0 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -25,6 +25,7 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem void parse_and_publish(const std::vector &data) override; void dump_config() override; using transform_func_t = std::function(ModbusSensor *, float, const std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } protected: diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index e03b0d37be..df11b268ac 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -3,21 +3,25 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - MODBUS_REGISTER_TYPE, - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, + MODBUS_REGISTER_TYPE, ) from ..const import ( CONF_BITMASK, - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_WRITE_LAMBDA, ) +CONF_USE_WRITE_MULTIPLE = "use_write_multiple" DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] @@ -26,31 +30,23 @@ ModbusSwitch = modbus_controller_ns.class_( "ModbusSwitch", cg.Component, switch.Switch, SensorItem ) - CONFIG_SCHEMA = cv.All( - switch.SWITCH_SCHEMA.extend( + switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusSwitch), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, - cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, _ = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], @@ -63,19 +59,18 @@ async def to_code(config): await switch.register_switch(var, config) paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) - cg.add(paren.add_sensor_item(var)) cg.add(var.set_parent(paren)) - if CONF_LAMBDA in config: - publish_template_ = await cg.process_lambda( - config[CONF_LAMBDA], + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + cg.add(paren.add_sensor_item(var)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], [ (ModbusSwitch.operator("ptr"), "item"), - (bool, "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), ], return_type=cg.optional.template(bool), ) - cg.add(var.set_template(publish_template_)) + cg.add(var.set_write_template(template_)) + await add_modbus_base_properties(var, config, ModbusSwitch, bool, bool) diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index ce9557e6c4..c7c3c419d4 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -45,22 +45,50 @@ void ModbusSwitch::parse_and_publish(const std::vector &data) { void ModbusSwitch::write_state(bool state) { // This will be called every time the user requests a state change. ModbusCommandItem cmd; - ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), - ONOFF(state), (int) this->register_type, this->start_address, this->offset); - switch (this->register_type) { - case ModbusRegisterType::COIL: + 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 Switch write raw: %s", hexencode(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_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), + ONOFF(state), (int) this->register_type, this->start_address, this->offset); + if (this->register_type == ModbusRegisterType::COIL) { // offset for coil and discrete inputs is the coil/register number not bytes - cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); - break; - case ModbusRegisterType::DISCRETE_INPUT: - cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, state); - break; - - default: + 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); + } + } else { // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 - cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, - state ? 0xFFFF & this->bitmask : 0); - break; + if (this->use_write_multiple_) { + std::vector bool_states(1, state ? (0xFFFF & this->bitmask) : 0); + cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, 1, + bool_states); + } else { + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, + state ? 0xFFFF & this->bitmask : 0u); + } + } } this->parent_->queue_command(cmd); publish_state(state); diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index a38668fabb..5ac2af01a1 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -33,11 +33,16 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem void set_parent(ModbusController *parent) { this->parent_ = parent; } using transform_func_t = std::function(ModbusSwitch *, bool, const std::vector &)>; + using write_transform_func_t = std::function(ModbusSwitch *, bool, std::vector &)>; void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } + 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: ModbusController *parent_; + bool use_write_multiple_; optional publish_transform_func_{nullopt}; + optional write_transform_func_{nullopt}; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 2c02c86795..5cc85af5bc 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -3,15 +3,17 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - SensorItem, + add_modbus_base_properties, modbus_controller_ns, - ModbusController, + modbus_calc_properties, + validate_modbus_register, + ModbusItemBaseSchema, + SensorItem, MODBUS_REGISTER_TYPE, ) from ..const import ( - CONF_BYTE_OFFSET, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_COUNT, @@ -38,32 +40,23 @@ RAW_ENCODING = { } CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.TEXT_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( { cv.GenerateID(): cv.declare_id(ModbusTextSensor), - cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), - cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_OFFSET, default=0): cv.positive_int, - cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING), - cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, } - ).extend(cv.COMPONENT_SCHEMA), + ), + validate_modbus_register, ) async def to_code(config): - byte_offset = 0 - if CONF_OFFSET in config: - byte_offset = config[CONF_OFFSET] - # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET - if CONF_BYTE_OFFSET in config: - byte_offset = config[CONF_BYTE_OFFSET] + byte_offset, reg_count = modbus_calc_properties(config) response_size = config[CONF_RESPONSE_SIZE] reg_count = config[CONF_REGISTER_COUNT] if reg_count == 0: @@ -85,17 +78,6 @@ async def to_code(config): paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(paren.add_sensor_item(var)) - if CONF_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [ - (ModbusTextSensor.operator("ptr"), "item"), - (cg.std_string.operator("const").operator("ref"), "x"), - ( - cg.std_vector.template(cg.uint8).operator("const").operator("ref"), - "data", - ), - ], - return_type=cg.optional.template(cg.std_string), - ) - cg.add(var.set_template(template_)) + await add_modbus_base_properties( + var, config, ModbusTextSensor, cg.std_string, cg.std_string + )