From 24da1279d82f3ac775046487f369e8d3bbd7221b Mon Sep 17 00:00:00 2001 From: Martin Kennedy Date: Wed, 26 Mar 2025 23:11:52 -0400 Subject: [PATCH] Shove modbus_controller stuff to see if I can make it compile --- .../components/modbus_controller/__init__.py | 338 ++++++++ .../components/modbus_controller/automation.h | 35 + .../binary_sensor/__init__.py | 60 ++ .../binary_sensor/modbus_binarysensor.cpp | 38 + .../binary_sensor/modbus_binarysensor.h | 44 ++ esphome/components/modbus_controller/const.py | 21 + .../modbus_controller/modbus_controller.cpp | 722 ++++++++++++++++++ .../modbus_controller/modbus_controller.h | 556 ++++++++++++++ .../modbus_controller/number/__init__.py | 125 +++ .../number/modbus_number.cpp | 86 +++ .../modbus_controller/number/modbus_number.h | 50 ++ .../modbus_controller/output/__init__.py | 105 +++ .../output/modbus_output.cpp | 111 +++ .../modbus_controller/output/modbus_output.h | 76 ++ .../modbus_controller/select/__init__.py | 143 ++++ .../select/modbus_select.cpp | 90 +++ .../modbus_controller/select/modbus_select.h | 54 ++ .../modbus_controller/sensor/__init__.py | 68 ++ .../sensor/modbus_sensor.cpp | 31 + .../modbus_controller/sensor/modbus_sensor.h | 37 + .../modbus_controller/switch/__init__.py | 77 ++ .../switch/modbus_switch.cpp | 104 +++ .../modbus_controller/switch/modbus_switch.h | 50 ++ .../modbus_controller/text_sensor/__init__.py | 84 ++ .../text_sensor/modbus_textsensor.cpp | 52 ++ .../text_sensor/modbus_textsensor.h | 44 ++ 26 files changed, 3201 insertions(+) create mode 100644 esphome/components/modbus_controller/__init__.py create mode 100644 esphome/components/modbus_controller/automation.h create mode 100644 esphome/components/modbus_controller/binary_sensor/__init__.py create mode 100644 esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp create mode 100644 esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h create mode 100644 esphome/components/modbus_controller/const.py create mode 100644 esphome/components/modbus_controller/modbus_controller.cpp create mode 100644 esphome/components/modbus_controller/modbus_controller.h create mode 100644 esphome/components/modbus_controller/number/__init__.py create mode 100644 esphome/components/modbus_controller/number/modbus_number.cpp create mode 100644 esphome/components/modbus_controller/number/modbus_number.h create mode 100644 esphome/components/modbus_controller/output/__init__.py create mode 100644 esphome/components/modbus_controller/output/modbus_output.cpp create mode 100644 esphome/components/modbus_controller/output/modbus_output.h create mode 100644 esphome/components/modbus_controller/select/__init__.py create mode 100644 esphome/components/modbus_controller/select/modbus_select.cpp create mode 100644 esphome/components/modbus_controller/select/modbus_select.h create mode 100644 esphome/components/modbus_controller/sensor/__init__.py create mode 100644 esphome/components/modbus_controller/sensor/modbus_sensor.cpp create mode 100644 esphome/components/modbus_controller/sensor/modbus_sensor.h create mode 100644 esphome/components/modbus_controller/switch/__init__.py create mode 100644 esphome/components/modbus_controller/switch/modbus_switch.cpp create mode 100644 esphome/components/modbus_controller/switch/modbus_switch.h create mode 100644 esphome/components/modbus_controller/text_sensor/__init__.py create mode 100644 esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp create mode 100644 esphome/components/modbus_controller/text_sensor/modbus_textsensor.h diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py new file mode 100644 index 0000000..2a08075 --- /dev/null +++ b/esphome/components/modbus_controller/__init__.py @@ -0,0 +1,338 @@ +import binascii + +from esphome import automation +import esphome.codegen as cg +from esphome.components import modbus +import esphome.config_validation as cv +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_LAMBDA, + CONF_NAME, + CONF_OFFSET, + CONF_TRIGGER_ID, +) +from esphome.cpp_helpers import logging + +from .const import ( + CONF_ALLOW_DUPLICATE_COMMANDS, + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_COMMAND_THROTTLE, + CONF_CUSTOM_COMMAND, + CONF_FORCE_NEW_RANGE, + CONF_MAX_CMD_RETRIES, + CONF_MODBUS_CONTROLLER_ID, + CONF_OFFLINE_SKIP_UPDATES, + CONF_ON_COMMAND_SENT, + CONF_ON_ONLINE, + CONF_ON_OFFLINE, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_RESPONSE_SIZE, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, +) + +CODEOWNERS = ["@martgras"] + +AUTO_LOAD = ["modbus"] + +CONF_READ_LAMBDA = "read_lambda" +CONF_SERVER_REGISTERS = "server_registers" +MULTI_CONF = True + +modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") +ModbusController = modbus_controller_ns.class_( + "ModbusController", cg.PollingComponent, modbus.ModbusDevice +) + +SensorItem = modbus_controller_ns.struct("SensorItem") +ServerRegister = modbus_controller_ns.struct("ServerRegister") + +ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") +MODBUS_FUNCTION_CODE = { + "read_coils": ModbusFunctionCode.READ_COILS, + "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, + "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, + "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, + "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, + "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, + "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, + "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, +} + +ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") +ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") + +MODBUS_WRITE_REGISTER_TYPE = { + "custom": ModbusRegisterType.CUSTOM, + "coil": ModbusRegisterType.COIL, + "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, + "read": ModbusRegisterType.READ, +} + +SensorValueType_ns = modbus_controller_ns.namespace("SensorValueType") +SensorValueType = SensorValueType_ns.enum("SensorValueType") +SENSOR_VALUE_TYPE = { + "RAW": SensorValueType.RAW, + "U_WORD": SensorValueType.U_WORD, + "S_WORD": SensorValueType.S_WORD, + "U_DWORD": SensorValueType.U_DWORD, + "U_DWORD_R": SensorValueType.U_DWORD_R, + "S_DWORD": SensorValueType.S_DWORD, + "S_DWORD_R": SensorValueType.S_DWORD_R, + "U_QWORD": SensorValueType.U_QWORD, + "U_QWORD_R": SensorValueType.U_QWORD_R, + "S_QWORD": SensorValueType.S_QWORD, + "S_QWORD_R": SensorValueType.S_QWORD_R, + "FP32": SensorValueType.FP32, + "FP32_R": SensorValueType.FP32_R, +} + +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORD_R": 4, + "S_QWORD": 4, + "S_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + +ModbusCommandSentTrigger = modbus_controller_ns.class_( + "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + +ModbusOnlineTrigger = modbus_controller_ns.class_( + "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + +ModbusOfflineTrigger = modbus_controller_ns.class_( + "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + +_LOGGER = logging.getLogger(__name__) + +ModbusServerRegisterSchema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ServerRegister), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + } +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ModbusController), + cv.Optional(CONF_ALLOW_DUPLICATE_COMMANDS, default=False): cv.boolean, + cv.Optional( + CONF_COMMAND_THROTTLE, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, + cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional( + CONF_SERVER_REGISTERS, + ): cv.ensure_list(ModbusServerRegisterSchema), + cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ModbusCommandSentTrigger + ), + } + ), + cv.Optional(CONF_ON_ONLINE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), + } + ), + cv.Optional(CONF_ON_OFFLINE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) + +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, + cv.Optional(CONF_RESPONSE_SIZE, default=0): cv.positive_int, + }, +) + + +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 _final_validate(config): + if CONF_SERVER_REGISTERS in config: + return modbus.final_validate_modbus_device("modbus_controller", role="server")( + config + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +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, lambda_param_type=cg.float_, lambda_return_type=float +): + if CONF_CUSTOM_COMMAND in config: + cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND])) + + if config[CONF_RESPONSE_SIZE] > 0: + cg.add(var.set_register_size(config[CONF_RESPONSE_SIZE])) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (sensor_type.operator("ptr"), "item"), + (lambda_param_type, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(lambda_return_type), + ) + cg.add(var.set_template(template_)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) + cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) + cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) + if CONF_SERVER_REGISTERS in config: + for server_register in config[CONF_SERVER_REGISTERS]: + cg.add( + var.add_server_register( + cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [], + return_type=cg.float_, + ), + ) + ) + ) + await register_modbus_device(var, config) + for conf in config.get(CONF_ON_COMMAND_SENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + ) + for conf in config.get(CONF_ON_ONLINE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + ) + for conf in config.get(CONF_ON_OFFLINE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + ) + + +async def register_modbus_device(var, config): + cg.add(var.set_address(config[CONF_ADDRESS])) + await cg.register_component(var, config) + return await modbus.register_modbus_device(var, config) + + +def function_code_to_register(function_code): + FUNCTION_CODE_TYPE_MAP = { + "read_coils": ModbusRegisterType.COIL, + "read_discrete_inputs": ModbusRegisterType.DISCRETE, + "read_holding_registers": ModbusRegisterType.HOLDING, + "read_input_registers": ModbusRegisterType.READ, + "write_single_coil": ModbusRegisterType.COIL, + "write_single_register": ModbusRegisterType.HOLDING, + "write_multiple_coils": ModbusRegisterType.COIL, + "write_multiple_registers": ModbusRegisterType.HOLDING, + } + return FUNCTION_CODE_TYPE_MAP[function_code] diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h new file mode 100644 index 0000000..b333819 --- /dev/null +++ b/esphome/components/modbus_controller/automation.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/modbus_controller/modbus_controller.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusCommandSentTrigger : public Trigger { + public: + ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_command_sent_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + +class ModbusOnlineTrigger : public Trigger { + public: + ModbusOnlineTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_online_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + +class ModbusOfflineTrigger : public Trigger { + public: + ModbusOfflineTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_offline_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py new file mode 100644 index 0000000..2ae008f --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID + +from .. import ( + MODBUS_REGISTER_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, +) +from ..const import ( + CONF_BITMASK, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusBinarySensor = modbus_controller_ns.class_( + "ModbusBinarySensor", cg.Component, binary_sensor.BinarySensor, SensorItem +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.binary_sensor_schema(ModbusBinarySensor) + .extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, _ = modbus_calc_properties(config) + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await binary_sensor.register_binary_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + await add_modbus_base_properties(var, config, ModbusBinarySensor, bool, bool) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp new file mode 100644 index 0000000..c3eb3d4 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -0,0 +1,38 @@ +#include "modbus_binarysensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.binary_sensor"; + +void ModbusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Modbus Controller Binary Sensor", this); } + +void ModbusBinarySensor::parse_and_publish(const std::vector &data) { + bool value; + + switch (this->register_type) { + case ModbusRegisterType::DISCRETE_INPUT: + case ModbusRegisterType::COIL: + // offset for coil is the actual number of the coil not the byte offset + value = coil_from_vector(this->offset, data); + break; + default: + value = get_data(data, this->offset) & this->bitmask; + break; + } + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } + } + this->publish_state(value); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h new file mode 100644 index 0000000..3a017c6 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem { + public: + ModbusBinarySensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + uint16_t skip_updates, bool force_new_range) { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + + if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) { + this->register_count = offset + 1; + } else { + this->register_count = 1; + } + } + + void parse_and_publish(const std::vector &data) override; + void set_state(bool state) { this->state = state; } + + void dump_config() override; + + using transform_func_t = std::function(ModbusBinarySensor *, bool, const std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + optional transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py new file mode 100644 index 0000000..4d39e48 --- /dev/null +++ b/esphome/components/modbus_controller/const.py @@ -0,0 +1,21 @@ +CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" +CONF_BITMASK = "bitmask" +CONF_BYTE_OFFSET = "byte_offset" +CONF_COMMAND_THROTTLE = "command_throttle" +CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" +CONF_CUSTOM_COMMAND = "custom_command" +CONF_FORCE_NEW_RANGE = "force_new_range" +CONF_MAX_CMD_RETRIES = "max_cmd_retries" +CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" +CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" +CONF_ON_COMMAND_SENT = "on_command_sent" +CONF_ON_ONLINE = "on_online" +CONF_ON_OFFLINE = "on_offline" +CONF_RAW_ENCODE = "raw_encode" +CONF_REGISTER_COUNT = "register_count" +CONF_REGISTER_TYPE = "register_type" +CONF_RESPONSE_SIZE = "response_size" +CONF_SKIP_UPDATES = "skip_updates" +CONF_USE_WRITE_MULTIPLE = "use_write_multiple" +CONF_VALUE_TYPE = "value_type" +CONF_WRITE_LAMBDA = "write_lambda" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp new file mode 100644 index 0000000..3f487ab --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -0,0 +1,722 @@ +#include "modbus_controller.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller"; + +void ModbusController::setup() { this->create_register_ranges_(); } + +/* + To work with the existing modbus class and avoid polling for responses a command queue is used. + send_next_command will submit the command at the top of the queue and set the corresponding callback + to handle the response from the device. + Once the response has been processed it is removed from the queue and the next command is sent +*/ +bool ModbusController::send_next_command_() { + uint32_t last_send = millis() - this->last_command_timestamp_; + + if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) { + auto &command = this->command_queue_.front(); + + // remove from queue if command was sent too often + if (!command->should_retry(this->max_cmd_retries_)) { + if (!this->module_offline_) { + ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_); + + if (this->offline_skip_updates_ > 0) { + // Update skip_updates_counter to stop flooding channel with timeouts + for (auto &r : this->register_ranges_) { + r.skip_updates_counter = this->offline_skip_updates_; + } + } + + this->module_offline_ = true; + this->offline_callback_.call((int) command->function_code, command->register_address); + } + ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue", + this->address_, command->register_address); + this->command_queue_.pop_front(); + } else { + ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, + command->register_address, command->register_count); + command->send(); + + this->last_command_timestamp_ = millis(); + + this->command_sent_callback_.call((int) command->function_code, command->register_address); + + // remove from queue if no handler is defined + if (!command->on_data_func) { + this->command_queue_.pop_front(); + } + } + } + return (!this->command_queue_.empty()); +} + +// Queue incoming response +void ModbusController::on_modbus_data(const std::vector &data) { + auto ¤t_command = this->command_queue_.front(); + if (current_command != nullptr) { + if (this->module_offline_) { + ESP_LOGW(TAG, "Modbus device=%d back online", this->address_); + + if (this->offline_skip_updates_ > 0) { + // Restore skip_updates_counter to restore commands updates + for (auto &r : this->register_ranges_) { + r.skip_updates_counter = 0; + } + } + // Restore module online state + this->module_offline_ = false; + this->online_callback_.call((int) current_command->function_code, current_command->register_address); + } + + // Move the commandItem to the response queue + current_command->payload = data; + this->incoming_queue_.push(std::move(current_command)); + ESP_LOGV(TAG, "Modbus response queued"); + this->command_queue_.pop_front(); + } +} + +// Dispatch the response to the registered handler +void ModbusController::process_modbus_data_(const ModbusCommandItem *response) { + ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address, + response->payload.size()); + response->on_data_func(response->register_type, response->register_address, response->payload); +} + +void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) { + ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code); + // Remove pending command waiting for a response + auto ¤t_command = this->command_queue_.front(); + if (current_command != nullptr) { + ESP_LOGE(TAG, + "Modbus error - last command: function code=0x%X register address = 0x%X " + "registers count=%d " + "payload size=%zu", + function_code, current_command->register_address, current_command->register_count, + current_command->payload.size()); + this->command_queue_.pop_front(); + } +} + +void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers) { + ESP_LOGD(TAG, + "Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + std::vector sixteen_bit_response; + 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) { + float value = server_register->read_lambda(); + + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, value); + std::vector payload = float_to_payload(value, server_register->value_type); + sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); + current_address += server_register->register_count; + found = true; + break; + } + } + + if (!found) { + ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); + std::vector error_response; + error_response.push_back(this->address_); + error_response.push_back(0x81); + error_response.push_back(0x02); + this->send_raw(error_response); + return; + } + } + + std::vector response; + for (auto v : sixteen_bit_response) { + auto decoded_value = decode_value(v); + response.push_back(decoded_value[0]); + response.push_back(decoded_value[1]); + } + + this->send(function_code, start_address, number_of_registers, response.size(), response.data()); +} + +SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { + auto reg_it = std::find_if( + std::begin(this->register_ranges_), std::end(this->register_ranges_), + [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); }); + + if (reg_it == this->register_ranges_.end()) { + ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address); + } else { + return reg_it->sensors; + } + + // not found + return {}; +} +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); + + // loop through all sensors with the same start address + auto sensors = find_sensors_(register_type, start_address); + for (auto *sensor : sensors) { + sensor->parse_and_publish(data); + } +} + +void ModbusController::queue_command(const ModbusCommandItem &command) { + if (!this->allow_duplicate_commands_) { + // check if this command is already qeued. + // not very effective but the queue is never really large + for (auto &item : this->command_queue_) { + if (item->is_equal(command)) { + ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", + static_cast(command.register_type), command.register_address, command.register_count); + // update the payload of the queued command + // replaces a previous command + item->payload = command.payload; + return; + } + } + } + this->command_queue_.push_back(make_unique(command)); +} + +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) { + // if a custom command is used the user supplied custom_data is only available in the SensorItem. + if (r.register_type == ModbusRegisterType::CUSTOM) { + auto sensors = this->find_sensors_(r.register_type, r.start_address); + if (!sensors.empty()) { + auto sensor = sensors.cbegin(); + auto command_item = ModbusCommandItem::create_custom_command( + this, (*sensor)->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 = (*sensor)->start_address; + command_item.register_count = (*sensor)->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--; + } +} +// +// Queue the modbus requests to be send. +// Once we get a response to the command it is removed from the queue and the next command is send +// +void ModbusController::update() { + if (!this->command_queue_.empty()) { + ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size()); + } else { + ESP_LOGV(TAG, "Updating modbus component"); + } + + for (auto &r : this->register_ranges_) { + ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address); + update_range_(r); + } +} + +// walk through the sensors and determine the register ranges to read +size_t ModbusController::create_register_ranges_() { + this->register_ranges_.clear(); + if (this->parent_->role == modbus::ModbusRole::CLIENT && this->sensorset_.empty()) { + ESP_LOGW(TAG, "No sensors registered"); + return 0; + } + + // iterator is sorted see SensorItemsComparator for details + auto ix = this->sensorset_.begin(); + RegisterRange r = {}; + uint8_t buffer_offset = 0; + SensorItem *prev = nullptr; + while (ix != this->sensorset_.end()) { + SensorItem *curr = *ix; + + ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, + curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr); + + if (r.register_count == 0) { + // this is the first register in range + r.start_address = curr->start_address; + r.register_count = curr->register_count; + r.register_type = curr->register_type; + r.sensors.insert(curr); + r.skip_updates = curr->skip_updates; + r.skip_updates_counter = 0; + buffer_offset = curr->get_register_size(); + + ESP_LOGV(TAG, "Started new range"); + } else { + // this is not the first register in range so it might be possible + // to reuse the last register or extend the current range + if (!curr->force_new_range && r.register_type == curr->register_type && + curr->register_type != ModbusRegisterType::CUSTOM) { + if (curr->start_address == (r.start_address + r.register_count - prev->register_count) && + curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) { + // this register can re-use the data from the previous register + + // remove this sensore because start_address is changed (sort-order) + ix = this->sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += prev->offset; + + this->sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } else if (curr->start_address == (r.start_address + r.register_count)) { + // this register can extend the current range + + // remove this sensore because start_address is changed (sort-order) + ix = this->sensorset_.erase(ix); + + curr->start_address = r.start_address; + curr->offset += buffer_offset; + buffer_offset += curr->get_register_size(); + r.register_count += curr->register_count; + + this->sensorset_.insert(curr); + // move iterator backwards because it will be incremented later + ix--; + + ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address, + curr->register_count, curr->offset); + } + } + } + + if (curr->start_address == r.start_address && curr->register_type == r.register_type) { + // use the lowest non zero value for the whole range + // Because zero is the default value for skip_updates it is excluded from getting the min value. + if (curr->skip_updates != 0) { + if (r.skip_updates != 0) { + r.skip_updates = std::min(r.skip_updates, curr->skip_updates); + } else { + r.skip_updates = curr->skip_updates; + } + } + + // add sensor to this range + r.sensors.insert(curr); + + ix++; + } else { + ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + this->register_ranges_.push_back(r); + r = {}; + buffer_offset = 0; + // do not increment the iterator here because the current sensor has to be re-evaluated + } + + prev = curr; + } + + if (r.register_count > 0) { + // Add the last range + ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + this->register_ranges_.push_back(r); + } + + return this->register_ranges_.size(); +} + +void ModbusController::dump_config() { + ESP_LOGCONFIG(TAG, "ModbusController:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + ESP_LOGCONFIG(TAG, " Max Command Retries: %d", this->max_cmd_retries_); + ESP_LOGCONFIG(TAG, " Offline Skip Updates: %d", this->offline_skip_updates_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGCONFIG(TAG, "sensormap"); + for (auto &it : this->sensorset_) { + ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d", + static_cast(it->register_type), it->start_address, it->offset, it->register_count, + it->get_register_size()); + } + ESP_LOGCONFIG(TAG, "ranges"); + for (auto &it : this->register_ranges_) { + ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), + it.start_address, it.register_count, it.skip_updates); + } + ESP_LOGCONFIG(TAG, "server registers"); + for (auto &r : this->server_registers_) { + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address, + static_cast(r->value_type), r->register_count); + } +#endif +} + +void ModbusController::loop() { + // Incoming data to process? + if (!this->incoming_queue_.empty()) { + auto &message = this->incoming_queue_.front(); + if (message != nullptr) + this->process_modbus_data_(message.get()); + this->incoming_queue_.pop(); + + } else { + // all messages processed send pending commands + this->send_next_command_(); + } +} + +void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data(data, 0), get_data(data, 1)); +} + +void ModbusController::dump_sensors_() { + ESP_LOGV(TAG, "sensors"); + for (auto &it : this->sensorset_) { + ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count, + it->get_register_size(), it->offset); + } +} + +ModbusCommandItem ModbusCommandItem::create_read_command( + ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> + &&handler) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = register_type; + cmd.function_code = modbus_register_read_function(register_type); + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = std::move(handler); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbusdevice, + ModbusRegisterType register_type, uint16_t start_address, + uint16_t register_count) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = register_type; + cmd.function_code = modbus_register_read_function(register_type); + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_register_data(register_type, start_address, data); + }; + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusController *modbusdevice, + uint16_t start_address, uint16_t register_count, + const std::vector &values) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::HOLDING; + cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS; + cmd.register_address = start_address; + cmd.register_count = register_count; + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + for (auto v : values) { + auto decoded_value = decode_value(v); + cmd.payload.push_back(decoded_value[0]); + cmd.payload.push_back(decoded_value[1]); + } + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_single_coil(ModbusController *modbusdevice, uint16_t address, + bool value) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::COIL; + cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_COIL; + cmd.register_address = address; + cmd.register_count = 1; + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + cmd.payload.push_back(value ? 0xFF : 0); + cmd.payload.push_back(0); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, + const std::vector &values) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::COIL; + cmd.function_code = ModbusFunctionCode::WRITE_MULTIPLE_COILS; + cmd.register_address = start_address; + cmd.register_count = values.size(); + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + + uint8_t bitmask = 0; + int bitcounter = 0; + for (auto coil : values) { + if (coil) { + bitmask |= (1 << bitcounter); + } + bitcounter++; + if (bitcounter % 8 == 0) { + cmd.payload.push_back(bitmask); + bitmask = 0; + } + } + // add remaining bits + if (bitcounter % 8) { + cmd.payload.push_back(bitmask); + } + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, + uint16_t value) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.register_type = ModbusRegisterType::HOLDING; + cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_REGISTER; + cmd.register_address = start_address; + cmd.register_count = 1; // not used here anyways + cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + modbusdevice->on_write_register_response(cmd.register_type, start_address, data); + }; + + auto decoded_value = decode_value(value); + cmd.payload.push_back(decoded_value[0]); + cmd.payload.push_back(decoded_value[1]); + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler) { + ModbusCommandItem cmd; + cmd.modbusdevice = modbusdevice; + cmd.function_code = ModbusFunctionCode::CUSTOM; + if (handler == nullptr) { + cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + ESP_LOGI(TAG, "Custom Command sent"); + }; + } else { + cmd.on_data_func = handler; + } + cmd.payload = values; + + return cmd; +} + +ModbusCommandItem ModbusCommandItem::create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler) { + ModbusCommandItem cmd = {}; + cmd.modbusdevice = modbusdevice; + cmd.function_code = ModbusFunctionCode::CUSTOM; + if (handler == nullptr) { + cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + ESP_LOGI(TAG, "Custom Command sent"); + }; + } else { + cmd.on_data_func = handler; + } + for (auto v : values) { + cmd.payload.push_back((v >> 8) & 0xFF); + cmd.payload.push_back(v & 0xFF); + } + + return cmd; +} + +bool ModbusCommandItem::send() { + if (this->function_code != ModbusFunctionCode::CUSTOM) { + modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), + this->payload.empty() ? nullptr : &this->payload[0]); + } else { + modbusdevice->send_raw(this->payload); + } + this->send_count_++; + ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address, + this->register_count, this->send_count_); + return true; +} + +bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { + // for custom commands we have to check for identical payloads, since + // address/count/type fields will be set to zero + return this->function_code == ModbusFunctionCode::CUSTOM + ? this->payload == other.payload + : other.register_address == this->register_address && other.register_count == this->register_count && + other.register_type == this->register_type && other.function_code == this->function_code; +} + +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::FP32: + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + data.push_back((value & 0xFFFF000000000000) >> 48); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF000000000000) >> 48); + break; + default: + ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d", + static_cast(value_type)); + break; + } +} + +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits + + size_t size = data.size() - offset; + bool error = false; + switch (sensor_value_type) { + case SensorValueType::U_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::U_DWORD: + case SensorValueType::FP32: + if (size >= 4) { + value = get_data(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::FP32_R: + if (size >= 4) { + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data(data, offset), + bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::S_DWORD: + if (size >= 4) { + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_DWORD_R: { + if (size >= 4) { + value = get_data(data, offset); + // Currently the high word is at the low position + // the sign bit is therefore at low before the switch + uint32_t sign_bit = (value & 0x8000) << 16; + value = mask_and_shift_by_rightbit( + static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); + } else { + error = true; + } + } break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + // Ignore bitmask for QWORD + if (size >= 8) { + value = get_data(data, offset); + } else { + error = true; + } + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: { + // Ignore bitmask for QWORD + if (size >= 8) { + uint64_t tmp = get_data(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); + } else { + error = true; + } + } break; + case SensorValueType::RAW: + default: + break; + } + if (error) + ESP_LOGE(TAG, "not enough data for value"); + return value; +} + +void ModbusController::add_on_command_sent_callback(std::function &&callback) { + this->command_sent_callback_.add(std::move(callback)); +} + +void ModbusController::add_on_online_callback(std::function &&callback) { + this->online_callback_.add(std::move(callback)); +} + +void ModbusController::add_on_offline_callback(std::function &&callback) { + this->offline_callback_.add(std::move(callback)); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h new file mode 100644 index 0000000..dfd52e4 --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -0,0 +1,556 @@ +#pragma once + +#include "esphome/core/component.h" + +#include "esphome/components/modbus/modbus.h" +#include "esphome/core/automation.h" + +#include +#include +#include +#include +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusController; + +enum class ModbusFunctionCode { + CUSTOM = 0x00, + READ_COILS = 0x01, + READ_DISCRETE_INPUTS = 0x02, + READ_HOLDING_REGISTERS = 0x03, + READ_INPUT_REGISTERS = 0x04, + WRITE_SINGLE_COIL = 0x05, + WRITE_SINGLE_REGISTER = 0x06, + READ_EXCEPTION_STATUS = 0x07, // not implemented + DIAGNOSTICS = 0x08, // not implemented + GET_COMM_EVENT_COUNTER = 0x0B, // not implemented + GET_COMM_EVENT_LOG = 0x0C, // not implemented + WRITE_MULTIPLE_COILS = 0x0F, + WRITE_MULTIPLE_REGISTERS = 0x10, + REPORT_SERVER_ID = 0x11, // not implemented + READ_FILE_RECORD = 0x14, // not implemented + WRITE_FILE_RECORD = 0x15, // not implemented + MASK_WRITE_REGISTER = 0x16, // not implemented + READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented + READ_FIFO_QUEUE = 0x18, // not implemented +}; + +enum class ModbusRegisterType : uint8_t { + CUSTOM = 0x0, + COIL = 0x01, + DISCRETE_INPUT = 0x02, + HOLDING = 0x03, + READ = 0x04, +}; + +enum class SensorValueType : uint8_t { + RAW = 0x00, // variable length + U_WORD = 0x1, // 1 Register unsigned + U_DWORD = 0x2, // 2 Registers unsigned + S_WORD = 0x3, // 1 Register signed + S_DWORD = 0x4, // 2 Registers signed + BIT = 0x5, + U_DWORD_R = 0x6, // 2 Registers unsigned + S_DWORD_R = 0x7, // 2 Registers unsigned + U_QWORD = 0x8, + S_QWORD = 0x9, + U_QWORD_R = 0xA, + S_QWORD_R = 0xB, + FP32 = 0xC, + FP32_R = 0xD +}; + +inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::READ_COILS; + break; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::READ_DISCRETE_INPUTS; + break; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_HOLDING_REGISTERS; + break; + case ModbusRegisterType::READ: + return ModbusFunctionCode::READ_INPUT_REGISTERS; + break; + default: + return ModbusFunctionCode::CUSTOM; + break; + } +} +inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::WRITE_SINGLE_COIL; + break; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::CUSTOM; + break; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; + break; + case ModbusRegisterType::READ: + default: + return ModbusFunctionCode::CUSTOM; + break; + } +} + +inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } + +/** Get a byte from a hex string + * hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 + * hex_byte_from_str("1122",0) returns 0x11 + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return byte value + */ +inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { + if (value.length() < pos * 2 + 1) + return 0; + return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); +} + +/** Get a word from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return word value + */ +inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { + return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); +} + +/** Get a dword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return dword value + */ +inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { + return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); +} + +/** Get a qword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return qword value + */ +inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { + return static_cast(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); +} + +// Extract data from modbus response buffer +/** Extract data from modbus response buffer + * @param T one of supported integer data types int_8,int_16,int_32,int_64 + * @param data modbus response buffer (uint8_t) + * @param buffer_offset offset in bytes. + * @return value of type T extracted from buffer + */ +template T get_data(const std::vector &data, size_t buffer_offset) { + if (sizeof(T) == sizeof(uint8_t)) { + return T(data[buffer_offset]); + } + if (sizeof(T) == sizeof(uint16_t)) { + return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); + } + + if (sizeof(T) == sizeof(uint32_t)) { + return get_data(data, buffer_offset) << 16 | get_data(data, (buffer_offset + 2)); + } + + if (sizeof(T) == sizeof(uint64_t)) { + return static_cast(get_data(data, buffer_offset)) << 32 | + (static_cast(get_data(data, buffer_offset + 4))); + } +} + +/** Extract coil data from modbus response buffer + * Responses for coil are packed into bytes . + * coil 3 is bit 3 of the first response byte + * coil 9 is bit 2 of the second response byte + * @param coil number of the cil + * @param data modbus response buffer (uint8_t) + * @return content of coil register + */ +inline bool coil_from_vector(int coil, const std::vector &data) { + auto data_byte = coil / 8; + return (data[data_byte] & (1 << (coil % 8))) > 0; +} + +/** Extract bits from value and shift right according to the bitmask + * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. + * the result is then shifted right by the position if the first right set bit in the mask + * Useful for modbus data where more than one value is packed in a 16 bit register + * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as + * D15 - D8 = hour, D7 - D0 = minute + * To get the hours use mask 0xFF00 and 0x00FF for the minute + * @param data an integral value between 16 aand 32 bits, + * @param bitmask the bitmask to apply + */ +template N mask_and_shift_by_rightbit(N data, uint32_t mask) { + auto result = (mask & data); + if (result == 0 || mask == 0xFFFFFFFF) { + return result; + } + for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { + if ((mask & (1 << pos)) != 0) + return result >> pos; + } + return 0; +} + +/** Convert float value to vector suitable for sending + * @param data target for payload + * @param value float value to convert + * @param value_type defines if 16/32 or FP32 is used + * @return vector containing the modbus register words in correct order + */ +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type); + +/** Convert vector response payload to number. + * @param data payload with the data to convert + * @param sensor_value_type defines if 16/32/64 bits or FP32 is used + * @param offset offset to the data in data + * @param bitmask bitmask used for masking and shifting + * @return 64-bit number of the payload + */ +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); + +class ModbusController; + +class SensorItem { + public: + virtual void parse_and_publish(const std::vector &data) = 0; + + void set_custom_data(const std::vector &data) { custom_data = data; } + size_t virtual get_register_size() const { + if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) { + return 1; + } else { // if CONF_RESPONSE_BYTES is used override the default + return response_bytes > 0 ? response_bytes : register_count * 2; + } + } + // Override register size for modbus devices not using 1 register for one dword + void set_register_size(uint8_t register_size) { response_bytes = register_size; } + ModbusRegisterType register_type{ModbusRegisterType::CUSTOM}; + SensorValueType sensor_value_type{SensorValueType::RAW}; + uint16_t start_address{0}; + uint32_t bitmask{0}; + uint8_t offset{0}; + uint8_t register_count{0}; + uint8_t response_bytes{0}; + uint16_t skip_updates{0}; + std::vector custom_data{}; + bool force_new_range{false}; +}; + +class ServerRegister { + public: + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, + std::function read_lambda) { + this->address = address; + this->value_type = value_type; + this->register_count = register_count; + this->read_lambda = std::move(read_lambda); + } + uint16_t address{0}; + SensorValueType value_type{SensorValueType::RAW}; + uint8_t register_count{0}; + std::function read_lambda; +}; + +// ModbusController::create_register_ranges_ tries to optimize register range +// for this the sensors must be ordered by register_type, start_address and bitmask +class SensorItemsComparator { + public: + bool operator()(const SensorItem *lhs, const SensorItem *rhs) const { + // first sort according to register type + if (lhs->register_type != rhs->register_type) { + return lhs->register_type < rhs->register_type; + } + + // ensure that sensor with force_new_range set are before the others + if (lhs->force_new_range != rhs->force_new_range) { + return lhs->force_new_range > rhs->force_new_range; + } + + // sort by start address + if (lhs->start_address != rhs->start_address) { + return lhs->start_address < rhs->start_address; + } + + // sort by offset (ensures update of sensors in ascending order) + if (lhs->offset != rhs->offset) { + return lhs->offset < rhs->offset; + } + + // The pointer to the sensor is used last to ensure that + // multiple sensors with the same values can be added with a stable sort order. + return lhs < rhs; + } +}; + +using SensorSet = std::set; + +struct RegisterRange { + uint16_t start_address; + ModbusRegisterType register_type; + uint8_t register_count; + uint16_t skip_updates; // the config value + SensorSet sensors; // all sensors of this range + uint16_t skip_updates_counter; // the running value +}; + +class ModbusCommandItem { + public: + static const size_t MAX_PAYLOAD_BYTES = 240; + ModbusController *modbusdevice{nullptr}; + uint16_t register_address{0}; + uint16_t register_count{0}; + ModbusFunctionCode function_code{ModbusFunctionCode::CUSTOM}; + ModbusRegisterType register_type{ModbusRegisterType::CUSTOM}; + std::function &data)> + on_data_func; + std::vector payload = {}; + bool send(); + /// Check if the command should be retried based on the max_retries parameter + bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; }; + + /// factory methods + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param handler function called when the response is received + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_read_command( + ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, + std::function &data)> + &&handler); + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type, + uint16_t start_address, uint16_t register_count); + /** Create modbus read command + * Function code 02-04 + * @param modbusdevice pointer to the device to execute the command + * @param function_code modbus function code for the read command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param handler function called when the response is received + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address, + uint16_t register_count, const std::vector &values); + /** Create modbus write multiple registers command + * Function 16 (10hex) Write Multiple Registers + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param register_count number of registers to read + * @param value uint16_t single register value to write + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, + uint16_t value); + /** Create modbus write single registers command + * Function 05 (05hex) Write Single Coil + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param value uint16_t data to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value); + + /** Create modbus write multiple registers command + * Function 15 (0Fhex) Write Multiple Coils + * @param modbusdevice pointer to the device to execute the command + * @param start_address modbus address of the first register to read + * @param value bool vector of values to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, + const std::vector &values); + /** Create custom modbus command + * @param modbusdevice pointer to the device to execute the command + * @param values byte vector of data to be sent to the device. The complete payload must be provided with the + * exception of the crc codes + * @param handler function called when the response is received. Default is just logging a response + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler = nullptr); + + /** Create custom modbus command + * @param modbusdevice pointer to the device to execute the command + * @param values word vector of data to be sent to the device. The complete payload must be provided with the + * exception of the crc codes + * @param handler function called when the response is received. Default is just logging a response + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler = nullptr); + + bool is_equal(const ModbusCommandItem &other); + + protected: + // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats. + /// How many times this command has been sent + uint8_t send_count_{0}; +}; + +/** Modbus controller class. + * Each instance handles the modbus commuinication for all sensors with the same modbus address + * + * all sensor items (sensors, switches, binarysensor ...) are parsed in modbus address ranges. + * when esphome calls ModbusController::Update the commands for each range are created and sent + * Responses for the commands are dispatched to the modbus sensor items. + */ + +class ModbusController : public PollingComponent, public modbus::ModbusDevice { + public: + void dump_config() override; + void loop() override; + void setup() override; + void update() override; + + /// queues a modbus command in the send queue + void queue_command(const ModbusCommandItem &command); + /// Registers a sensor with the controller. Called by esphomes code generator + void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } + /// Registers a server register with the controller. Called by esphomes code generator + void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); } + /// called when a modbus response was parsed without errors + void on_modbus_data(const std::vector &data) override; + /// called when a modbus error response was received + void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; + /// called when a modbus request (function code 3 or 4) was parsed without errors + void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; + /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue + void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); + /// default delegate called by process_modbus_data when a response for a write response has retrieved from the + /// incoming queue + void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data); + /// Allow a duplicate command to be sent + void set_allow_duplicate_commands(bool allow_duplicate_commands) { + this->allow_duplicate_commands_ = allow_duplicate_commands; + } + /// get if a duplicate command can be sent + bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; } + /// called by esphome generated code to set the command_throttle period + void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; } + /// called by esphome generated code to set the offline_skip_updates + void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; } + /// get the number of queued modbus commands (should be mostly empty) + size_t get_command_queue_length() { return command_queue_.size(); } + /// get if the module is offline, didn't respond the last command + bool get_module_offline() { return module_offline_; } + /// Set callback for commands + void add_on_command_sent_callback(std::function &&callback); + /// Set callback for online changes + void add_on_online_callback(std::function &&callback); + /// Set callback for offline changes + void add_on_offline_callback(std::function &&callback); + /// called by esphome generated code to set the max_cmd_retries. + 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_; } + + 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 + SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const; + /// submit the read command for the address range to the send queue + void update_range_(RegisterRange &r); + /// parse incoming modbus data + void process_modbus_data_(const ModbusCommandItem *response); + /// send the next modbus command from the send queue + bool send_next_command_(); + /// dump the parsed sensormap for diagnostics + void dump_sensors_(); + /// Collection of all sensors for this component + SensorSet sensorset_; + /// Collection of all server registers for this component + std::vector server_registers_{}; + /// Continuous range of modbus registers + std::vector register_ranges_{}; + /// Hold the pending requests to be sent + std::list> command_queue_; + /// modbus response data waiting to get processed + std::queue> incoming_queue_; + /// if duplicate commands can be sent + bool allow_duplicate_commands_{false}; + /// when was the last send operation + uint32_t last_command_timestamp_{0}; + /// min time in ms between sending modbus commands + uint16_t command_throttle_{0}; + /// if module didn't respond the last command + bool module_offline_{false}; + /// how many updates to skip if module is offline + 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}; + /// Command sent callback + CallbackManager command_sent_callback_{}; + /// Server online callback + CallbackManager online_callback_{}; + /// Server offline callback + CallbackManager offline_callback_{}; +}; + +/** Convert vector response payload to float. + * @param data payload with data + * @param item SensorItem object + * @return float value of data + */ +inline float payload_to_float(const std::vector &data, const SensorItem &item) { + int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); + + float float_value; + if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { + float_value = bit_cast(static_cast(number)); + } else { + float_value = static_cast(number); + } + + return float_value; +} + +inline std::vector float_to_payload(float value, SensorValueType value_type) { + int64_t val; + + if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { + val = bit_cast(value); + } else { + val = llroundf(value); + } + + std::vector data; + number_to_payload(data, val, value_type); + return data; +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py new file mode 100644 index 0000000..b5efd7a --- /dev/null +++ b/esphome/components/modbus_controller/number/__init__.py @@ -0,0 +1,125 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MULTIPLY, + CONF_STEP, +) + +from .. import ( + MODBUS_WRITE_REGISTER_TYPE, + SENSOR_VALUE_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, +) +from ..const import ( + CONF_BITMASK, + CONF_CUSTOM_COMMAND, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusNumber = modbus_controller_ns.class_( + "ModbusNumber", cg.Component, number.Number, SensorItem +) + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + if config[CONF_MIN_VALUE] < -16777215: + raise cv.Invalid("max_value must be greater than -16777215") + if config[CONF_MAX_VALUE] > 16777215: + raise cv.Invalid("max_value must not be greater than 16777215") + 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(ModbusNumber) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum( + MODBUS_WRITE_REGISTER_TYPE + ), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + # 24 bits are the maximum value for fp32 before precision is lost + # 0x00FFFFFF = 16777215 + cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, + cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_, + cv.Optional(CONF_STEP, default=1): cv.positive_float, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + validate_min_max, + validate_modbus_number, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_VALUE_TYPE], + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) + + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + + cg.add(var.set_parent(parent)) + cg.add(parent.add_sensor_item(var)) + await add_modbus_base_properties(var, config, ModbusNumber) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusNumber.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/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp new file mode 100644 index 0000000..ea8467d --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -0,0 +1,86 @@ +#include +#include "modbus_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus.number"; + +void ModbusNumber::parse_and_publish(const std::vector &data) { + float result = payload_to_float(data, *this) / this->multiply_by_; + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, result, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + result = val.value(); + } + } + ESP_LOGD(TAG, "Number new state : %.02f", result); + // this->sensor_->raw_state = result; + this->publish_state(result); +} + +void ModbusNumber::control(float value) { + ModbusCommandItem write_cmd; + std::vector data; + float write_value = value; + // 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, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + write_value = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } else { + write_value = this->multiply_by_ * write_value; + } + + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str()); + write_cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); + }); + } else { + data = float_to_payload(write_value, this->sensor_value_type); + + ESP_LOGD(TAG, + "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", + this->get_name().c_str(), this->start_address, this->register_count, value, write_value); + + // Create and send the write command + if (this->register_count == 1 && !this->use_write_multiple_) { + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2, + data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command( + this->parent_, this->start_address + this->offset / 2, this->register_count, data); + } + // publish new value + write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + // gets called when the write command is ack'd from the device + this->parent_->on_write_register_response(write_cmd.register_type, start_address, data); + this->publish_state(value); + }; + } + this->parent_->queue_command(write_cmd); + this->publish_state(value); +} +void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h new file mode 100644 index 0000000..8f77b2e --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +using value_to_data_t = std::function(float); + +class ModbusNumber : public number::Number, public Component, public SensorItem { + public: + ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->register_count = register_count; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + }; + + void dump_config() override; + void parse_and_publish(const std::vector &data) override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { this->multiply_by_ = factor; } + + using transform_func_t = std::function(ModbusNumber *, float, const std::vector &)>; + using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; + void set_template(transform_func_t &&f) { this->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: + void control(float value) override; + optional transform_func_{nullopt}; + optional write_transform_func_{nullopt}; + ModbusController *parent_{nullptr}; + float multiply_by_{1.0}; + bool use_write_multiple_{false}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py new file mode 100644 index 0000000..1800a90 --- /dev/null +++ b/esphome/components/modbus_controller/output/__init__.py @@ -0,0 +1,105 @@ +import esphome.codegen as cg +from esphome.components import output +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY + +from .. import ( + SENSOR_VALUE_TYPE, + ModbusItemBaseSchema, + SensorItem, + modbus_calc_properties, + modbus_controller_ns, +) +from ..const import ( + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +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.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) + # Binary Output + write_template = None + if config[CONF_REGISTER_TYPE] == "coil": + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + ) + if CONF_WRITE_LAMBDA in config: + write_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: + write_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) + 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 write_template: + cg.add(var.set_write_template(write_template)) diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp new file mode 100644 index 0000000..f0f6e64 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -0,0 +1,111 @@ +#include "modbus_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.output"; + +/** Write a value to the device + * + */ +void ModbusFloatOutput::write_state(float value) { + std::vector data; + auto original_value = value; + // 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, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } else { + ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); + return; + } + } else { + value = this->multiply_by_ * value; + } + // lambda didn't set payload + if (data.empty()) { + data = float_to_payload(value, this->sensor_value_type); + } + + 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 + ModbusCommandItem write_cmd; + if (this->register_count == 1 && !this->use_write_multiple_) { + write_cmd = + ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset, + this->register_count, data); + } + this->parent_->queue_command(write_cmd); +} + +void ModbusFloatOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Float Output:"); + LOG_FLOAT_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)); +} + +// 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(this->parent_, this->start_address + this->offset, states); + } else { + cmd = ModbusCommandItem::create_write_single_coil(this->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 +} // namespace esphome diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h new file mode 100644 index 0000000..bceb97a --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem { + public: + ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) { + this->register_type = ModbusRegisterType::HOLDING; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->register_count = register_count; + this->sensor_value_type = value_type; + this->skip_updates = 0; + this->start_address += offset; + this->offset = 0; + } + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { this->multiply_by_ = factor; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + 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; } + + protected: + void write_state(float value) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_{nullptr}; + float multiply_by_{1.0}; + bool use_write_multiple_{false}; +}; + +class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem { + public: + ModbusBinaryOutput(uint16_t start_address, uint8_t offset) { + 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_{nullptr}; + bool use_write_multiple_{false}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py new file mode 100644 index 0000000..c94532d --- /dev/null +++ b/esphome/components/modbus_controller/select/__init__.py @@ -0,0 +1,143 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC + +from .. import ( + SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, + ModbusController, + SensorItem, + modbus_controller_ns, +) +from ..const import ( + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras", "@stegm"] +CONF_OPTIONSMAP = "optionsmap" + +ModbusSelect = modbus_controller_ns.class_( + "ModbusSelect", cg.Component, select.Select, SensorItem +) + + +def ensure_option_map(): + def validator(value): + cv.check_not_templatable(value) + option = cv.All(cv.string_strict) + mapping = cv.All(cv.int_range(-(2**63), 2**63 - 1)) + options_map_schema = cv.Schema({option: mapping}) + value = options_map_schema(value) + + all_values = list(value.values()) + unique_values = set(value.values()) + if len(all_values) != len(unique_values): + raise cv.Invalid("Mapping values must be unique.") + + return value + + return validator + + +def register_count_value_type_min(value): + reg_count = value.get(CONF_REGISTER_COUNT) + if reg_count is not None: + value_type = value[CONF_VALUE_TYPE] + min_register_count = TYPE_REGISTER_MAP[value_type] + if min_register_count > reg_count: + raise cv.Invalid( + f"Value type {value_type} needs at least {min_register_count} registers" + ) + return value + + +INTEGER_SENSOR_VALUE_TYPE = { + key: value for key, value in SENSOR_VALUE_TYPE.items() if not key.startswith("FP") +} + +CONFIG_SCHEMA = cv.All( + select.select_schema(ModbusSelect) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + INTEGER_SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Required(CONF_OPTIONSMAP): ensure_option_map(), + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + }, + ), + register_count_value_type_min, +) + + +async def to_code(config): + value_type = config[CONF_VALUE_TYPE] + reg_count = config.get(CONF_REGISTER_COUNT) + if reg_count is None: + reg_count = TYPE_REGISTER_MAP[value_type] + + options_map = config[CONF_OPTIONSMAP] + + var = cg.new_Pvariable( + config[CONF_ID], + value_type, + config[CONF_ADDRESS], + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + list(options_map.values()), + ) + + await cg.register_component(var, config) + await select.register_select(var, config, options=list(options_map.keys())) + + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(parent.add_sensor_item(var)) + cg.add(var.set_parent(parent)) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSelect.operator("const_ptr"), "item"), + (cg.int64, "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_)) + + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusSelect.operator("const_ptr"), "item"), + (cg.std_string.operator("const").operator("ref"), "x"), + (cg.int64, "value"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(cg.int64), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp new file mode 100644 index 0000000..56b8c78 --- /dev/null +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -0,0 +1,90 @@ +#include "modbus_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.select"; + +void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); } + +void ModbusSelect::parse_and_publish(const std::vector &data) { + int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); + + ESP_LOGD(TAG, "New select value %lld from payload", value); + + optional new_state; + + if (this->transform_func_.has_value()) { + auto val = (*this->transform_func_)(this, value, data); + if (val.has_value()) { + new_state = *val; + ESP_LOGV(TAG, "lambda returned option %s", new_state->c_str()); + } + } + + if (!new_state.has_value()) { + auto map_it = std::find(this->mapping_.cbegin(), this->mapping_.cend(), value); + + if (map_it != this->mapping_.cend()) { + size_t idx = std::distance(this->mapping_.cbegin(), map_it); + new_state = this->traits.get_options()[idx]; + ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); + } else { + ESP_LOGE(TAG, "No option found for mapping %lld", value); + } + } + + if (new_state.has_value()) { + this->publish_state(new_state.value()); + } +} + +void ModbusSelect::control(const std::string &value) { + auto options = this->traits.get_options(); + auto opt_it = std::find(options.cbegin(), options.cend(), value); + size_t idx = std::distance(options.cbegin(), opt_it); + optional mapval = this->mapping_[idx]; + ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); + + std::vector data; + + if (this->write_transform_func_.has_value()) { + auto val = (*this->write_transform_func_)(this, value, *mapval, data); + if (val.has_value()) { + mapval = *val; + ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); + } else { + ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control"); + return; + } + } + + if (data.empty()) { + number_to_payload(data, *mapval, this->sensor_value_type); + } else { + ESP_LOGV(TAG, "Using payload from write lambda"); + } + + if (data.empty()) { + ESP_LOGW(TAG, "No payload was created for updating select"); + return; + } + + const uint16_t write_address = this->start_address + this->offset / 2; + ModbusCommandItem write_cmd; + if ((this->register_count == 1) && (!this->use_write_multiple_)) { + write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, write_address, data[0]); + } else { + write_cmd = + ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, this->register_count, data); + } + + this->parent_->queue_command(write_cmd); + + if (this->optimistic_) + this->publish_state(value); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h new file mode 100644 index 0000000..55fb210 --- /dev/null +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/select/select.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusSelect : public Component, public select::Select, public SensorItem { + public: + ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint16_t skip_updates, + bool force_new_range, std::vector mapping) { + this->register_type = ModbusRegisterType::HOLDING; // not configurable + this->sensor_value_type = sensor_value_type; + this->start_address = start_address; + this->offset = 0; // not configurable + this->bitmask = 0xFFFFFFFF; // not configurable + this->register_count = register_count; + this->response_bytes = 0; // not configurable + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + this->mapping_ = std::move(mapping); + } + + using transform_func_t = + std::function(ModbusSelect *const, int64_t, const std::vector &)>; + using write_transform_func_t = + std::function(ModbusSelect *const, const std::string &, int64_t, std::vector &)>; + + void set_parent(ModbusController *const parent) { this->parent_ = parent; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + + void dump_config() override; + void parse_and_publish(const std::vector &data) override; + void control(const std::string &value) override; + + protected: + std::vector mapping_{}; + ModbusController *parent_{nullptr}; + bool use_write_multiple_{false}; + bool optimistic_{false}; + optional transform_func_{nullopt}; + optional write_transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py new file mode 100644 index 0000000..d8fce54 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID + +from .. import ( + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, +) +from ..const import ( + CONF_BITMASK, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusSensor = modbus_controller_ns.class_( + "ModbusSensor", cg.Component, sensor.Sensor, SensorItem +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(ModbusSensor) + .extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + 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, + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + value_type = config[CONF_VALUE_TYPE] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + value_type, + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + await add_modbus_base_properties(var, config, ModbusSensor) diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp new file mode 100644 index 0000000..a21fd91 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -0,0 +1,31 @@ + +#include "modbus_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.sensor"; + +void ModbusSensor::dump_config() { LOG_SENSOR(TAG, "Modbus Controller Sensor", this); } + +void ModbusSensor::parse_and_publish(const std::vector &data) { + float result = payload_to_float(data, *this); + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, result, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + result = val.value(); + } + } + ESP_LOGD(TAG, "Sensor new state: %.02f", result); + // this->sensor_->raw_state = result; + this->publish_state(result); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h new file mode 100644 index 0000000..65eb487 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusSensor : public Component, public sensor::Sensor, public SensorItem { + public: + ModbusSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->register_count = register_count; + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + } + + 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: + optional transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py new file mode 100644 index 0000000..258d87f --- /dev/null +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID + +from .. import ( + MODBUS_REGISTER_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, +) +from ..const import ( + CONF_BITMASK, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusSwitch = modbus_controller_ns.class_( + "ModbusSwitch", cg.Component, switch.Switch, SensorItem +) + +CONFIG_SCHEMA = cv.All( + switch.switch_schema(ModbusSwitch, default_restore_mode="DISABLED") + .extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + 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, + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, _ = modbus_calc_properties(config) + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(var.set_parent(paren)) + 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"), + (cg.bool_, "x"), + (cg.std_vector.template(cg.uint8).operator("ref"), "payload"), + ], + return_type=cg.optional.template(bool), + ) + 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 new file mode 100644 index 0000000..b729e26 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -0,0 +1,104 @@ + +#include "modbus_switch.h" +#include "esphome/core/log.h" +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.switch"; + +void ModbusSwitch::setup() { + optional initial_state = Switch::get_initial_state_with_restore_mode(); + if (initial_state.has_value()) { + // if it has a value, restore_mode is not "DISABLED", therefore act on the switch: + if (initial_state.value()) { + this->turn_on(); + } else { + this->turn_off(); + } + } +} +void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); } + +void ModbusSwitch::parse_and_publish(const std::vector &data) { + bool value = false; + switch (this->register_type) { + case ModbusRegisterType::DISCRETE_INPUT: + case ModbusRegisterType::COIL: + // offset for coil is the actual number of the coil not the byte offset + value = coil_from_vector(this->offset, data); + break; + default: + value = get_data(data, this->offset) & this->bitmask; + break; + } + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->publish_transform_func_) { + // the lambda can parse the response itself + auto val = (*this->publish_transform_func_)(this, value, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + value = val.value(); + } + } + + ESP_LOGV(TAG, "Publish '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), + ONOFF(value), (int) this->register_type, this->start_address, this->offset); + this->publish_state(value); +} + +void ModbusSwitch::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 Switch 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_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 + if (this->use_write_multiple_) { + std::vector states{state}; + cmd = ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states); + } else { + cmd = ModbusCommandItem::create_write_single_coil(this->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 + if (this->use_write_multiple_) { + std::vector bool_states(1, state ? (0xFFFF & this->bitmask) : 0); + cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset / 2, 1, + bool_states); + } else { + cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2, + state ? 0xFFFF & this->bitmask : 0u); + } + } + } + this->parent_->queue_command(cmd); + this->publish_state(state); +} +// ModbusSwitch end +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h new file mode 100644 index 0000000..fe4b7c1 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { + public: + ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + uint16_t skip_updates, bool force_new_range) { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = SensorValueType::BIT; + this->skip_updates = skip_updates; + this->register_count = 1; + if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) { + this->start_address += offset; + this->offset = 0; + } + this->force_new_range = force_new_range; + }; + void setup() override; + void write_state(bool state) override; + void dump_config() override; + void set_state(bool state) { this->state = state; } + void parse_and_publish(const std::vector &data) override; + 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_{nullptr}; + bool use_write_multiple_{false}; + optional publish_transform_func_{nullopt}; + optional write_transform_func_{nullopt}; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py new file mode 100644 index 0000000..35cae64 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -0,0 +1,84 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID + +from .. import ( + MODBUS_REGISTER_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, +) +from ..const import ( + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_RAW_ENCODE, + CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, + CONF_RESPONSE_SIZE, + CONF_SKIP_UPDATES, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusTextSensor = modbus_controller_ns.class_( + "ModbusTextSensor", cg.Component, text_sensor.TextSensor, SensorItem +) + +RawEncoding_ns = modbus_controller_ns.namespace("RawEncoding") +RawEncoding = RawEncoding_ns.enum("RawEncoding") +RAW_ENCODING = { + "NONE": RawEncoding.NONE, + "HEXBYTES": RawEncoding.HEXBYTES, + "COMMA": RawEncoding.COMMA, + "ANSI": RawEncoding.ANSI, +} + +CONFIG_SCHEMA = cv.All( + text_sensor.text_sensor_schema() + .extend(cv.COMPONENT_SCHEMA) + .extend(ModbusItemBaseSchema) + .extend( + { + cv.GenerateID(): cv.declare_id(ModbusTextSensor), + 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="ANSI"): cv.enum(RAW_ENCODING), + } + ), + validate_modbus_register, +) + + +async def to_code(config): + byte_offset, reg_count = modbus_calc_properties(config) + response_size = config[CONF_RESPONSE_SIZE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = response_size / 2 + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + reg_count, + config[CONF_RESPONSE_SIZE], + config[CONF_RAW_ENCODE], + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + ) + + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + await add_modbus_base_properties( + var, config, ModbusTextSensor, cg.std_string, cg.std_string + ) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp new file mode 100644 index 0000000..89e8674 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -0,0 +1,52 @@ + +#include "modbus_textsensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.text_sensor"; + +void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); } + +void ModbusTextSensor::parse_and_publish(const std::vector &data) { + std::string output_str{}; + uint8_t items_left = this->response_bytes; + uint8_t index = this->offset; + while ((items_left > 0) && index < data.size()) { + uint8_t b = data[index]; + switch (this->encode_) { + case RawEncoding::HEXBYTES: + output_str += str_snprintf("%02x", 2, b); + break; + case RawEncoding::COMMA: + output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b); + break; + case RawEncoding::ANSI: + if (b < 0x20) + break; + // FALLTHROUGH + // Anything else no encoding + default: + output_str += (char) b; + break; + } + items_left--; + index++; + } + + // Is there a lambda registered + // call it with the pre converted value and the raw data array + if (this->transform_func_.has_value()) { + // the lambda can parse the response itself + auto val = (*this->transform_func_)(this, output_str, data); + if (val.has_value()) { + ESP_LOGV(TAG, "Value overwritten by lambda"); + output_str = val.value(); + } + } + this->publish_state(output_str); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h new file mode 100644 index 0000000..d6eb5fd --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace modbus_controller { + +enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 }; + +class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem { + public: + ModbusTextSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, + uint16_t response_bytes, RawEncoding encode, uint16_t skip_updates, bool force_new_range) { + this->register_type = register_type; + this->start_address = start_address; + this->offset = offset; + this->response_bytes = response_bytes; + this->register_count = register_count; + this->encode_ = encode; + this->skip_updates = skip_updates; + this->bitmask = 0xFFFFFFFF; + this->sensor_value_type = SensorValueType::RAW; + this->force_new_range = force_new_range; + } + + void dump_config() override; + + void parse_and_publish(const std::vector &data) override; + using transform_func_t = + std::function(ModbusTextSensor *, std::string, const std::vector &)>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + optional transform_func_{nullopt}; + + RawEncoding encode_; +}; + +} // namespace modbus_controller +} // namespace esphome