diff --git a/CODEOWNERS b/CODEOWNERS index 92ee989309..17825a9205 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,13 @@ esphome/components/mcp9808/* @k7hpn esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey +esphome/components/modbus_controller/* @martgras +esphome/components/modbus_controller/binary_sensor/* @martgras +esphome/components/modbus_controller/number/* @martgras +esphome/components/modbus_controller/output/* @martgras +esphome/components/modbus_controller/sensor/* @martgras +esphome/components/modbus_controller/switch/* @martgras +esphome/components/modbus_controller/text_sensor/* @martgras esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw esphome/components/nextion/binary_sensor/* @senexcrenshaw diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 6b454cbaf0..254322d097 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -2,7 +2,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.cpp_helpers import gpio_pin_expression from esphome.components import uart -from esphome.const import CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_ADDRESS +from esphome.const import ( + CONF_FLOW_CONTROL_PIN, + CONF_ID, + CONF_ADDRESS, +) from esphome import pins DEPENDENCIES = ["uart"] @@ -13,11 +17,16 @@ ModbusDevice = modbus_ns.class_("ModbusDevice") MULTI_CONF = True CONF_MODBUS_ID = "modbus_id" +CONF_SEND_WAIT_TIME = "send_wait_time" + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Modbus), cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, + cv.Optional( + CONF_SEND_WAIT_TIME, default="250ms" + ): cv.positive_time_period_milliseconds, } ) .extend(cv.COMPONENT_SCHEMA) @@ -36,6 +45,9 @@ async def to_code(config): pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN]) cg.add(var.set_flow_control_pin(pin)) + if CONF_SEND_WAIT_TIME in config: + cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME])) + def modbus_device_schema(default_address): schema = { diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 2d714e72a2..1f6d868baf 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1,5 +1,6 @@ #include "modbus.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace modbus { @@ -13,10 +14,15 @@ void Modbus::setup() { } void Modbus::loop() { const uint32_t now = millis(); + if (now - this->last_modbus_byte_ > 50) { this->rx_buffer_.clear(); this->last_modbus_byte_ = now; } + // stop blocking new send commands after send_wait_time_ ms regardless if a response has been received since then + if (now - this->last_send_ > send_wait_time_) { + waiting_for_response = 0; + } while (this->available()) { uint8_t byte; @@ -49,48 +55,66 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; - + ESP_LOGV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); // Byte 0: modbus address (match all) if (at == 0) return true; uint8_t address = raw[0]; - - // Byte 1: Function (msb indicates error) - if (at == 1) - return (byte & 0x80) != 0x80; - + uint8_t function_code = raw[1]; // Byte 2: Size (with modbus rtu function code 4/3) // See also https://en.wikipedia.org/wiki/Modbus if (at == 2) return true; uint8_t data_len = raw[2]; - // Byte 3..3+data_len-1: Data - if (at < 3 + data_len) + uint8_t data_offset = 3; + // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } + + // Error ( msb indicates error ) + // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc + if ((function_code & 0x80) == 0x80) { + data_offset = 2; + data_len = 1; + } + + // Byte data_offset..data_offset+data_len-1: Data + if (at < data_offset + data_len) return true; // Byte 3+data_len: CRC_LO (over all bytes) - if (at == 3 + data_len) + if (at == data_offset + data_len) return true; - // Byte 3+len+1: CRC_HI (over all bytes) - uint16_t computed_crc = crc16(raw, 3 + data_len); - uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8); + + // Byte data_offset+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); if (computed_crc != remote_crc) { ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); return false; } - std::vector data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + waiting_for_response = 0; + std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { - device->on_modbus_data(data); + // Is it an error response? + if ((function_code & 0x80) == 0x80) { + ESP_LOGW(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); + device->on_modbus_error(function_code & 0x7F, raw[2]); + } else { + device->on_modbus_data(data); + } found = true; } } if (!found) { - ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); } // return false to reset buffer @@ -100,31 +124,79 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { void Modbus::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:"); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); + ESP_LOGCONFIG(TAG, " Send Wait Time: %d ms", this->send_wait_time_); } float Modbus::get_setup_priority() const { // After UART bus return setup_priority::BUS - 1.0f; } -void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { - uint8_t frame[8]; - frame[0] = address; - frame[1] = function; - frame[2] = start_address >> 8; - frame[3] = start_address >> 0; - frame[4] = register_count >> 8; - frame[5] = register_count >> 0; - auto crc = crc16(frame, 6); - frame[6] = crc >> 0; - frame[7] = crc >> 8; + +void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, + uint8_t payload_len, const uint8_t *payload) { + static const size_t MAX_VALUES = 128; + + if (number_of_entities > MAX_VALUES) { + ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); + return; + } + + std::vector data; + data.push_back(address); + data.push_back(function_code); + data.push_back(start_address >> 8); + data.push_back(start_address >> 0); + if (function_code != 0x5 && function_code != 0x6) { + data.push_back(number_of_entities >> 8); + data.push_back(number_of_entities >> 0); + } + + if (payload != nullptr) { + if (function_code == 0xF || function_code == 0x10) { // Write multiple + data.push_back(payload_len); // Byte count is required for write + } else { + payload_len = 2; // Write single register or coil + } + for (int i = 0; i < payload_len; i++) { + data.push_back(payload[i]); + } + } + + auto crc = crc16(data.data(), data.size()); + data.push_back(crc >> 0); + data.push_back(crc >> 8); if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(true); - this->write_array(frame, 8); + this->write_array(data); this->flush(); if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); + waiting_for_response = address; + last_send_ = millis(); + ESP_LOGV(TAG, "Modbus write: %s", hexencode(data).c_str()); +} + +// Helper function for lambdas +// Send raw command. Except CRC everything must be contained in payload +void Modbus::send_raw(const std::vector &payload) { + if (payload.empty()) { + return; + } + + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(true); + + auto crc = crc16(payload.data(), payload.size()); + this->write_array(payload); + this->write_byte(crc & 0xFF); + this->write_byte((crc >> 8) & 0xFF); + this->flush(); + if (this->flow_control_pin_ != nullptr) + this->flow_control_pin_->digital_write(false); + waiting_for_response = payload[0]; + last_send_ = millis(); } } // namespace modbus diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 876c46b688..400e29e08b 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -22,17 +22,21 @@ class Modbus : public uart::UARTDevice, public Component { float get_setup_priority() const override; - void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); - + void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities, + uint8_t payload_len = 0, const uint8_t *payload = nullptr); + void send_raw(const std::vector &payload); void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; } + uint8_t waiting_for_response{0}; + void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; } protected: GPIOPin *flow_control_pin_{nullptr}; bool parse_modbus_byte_(uint8_t byte); - + uint16_t send_wait_time_{250}; std::vector rx_buffer_; uint32_t last_modbus_byte_{0}; + uint32_t last_send_{0}; std::vector devices_; }; @@ -43,10 +47,14 @@ class ModbusDevice { void set_parent(Modbus *parent) { parent_ = parent; } void set_address(uint8_t address) { address_ = address; } virtual void on_modbus_data(const std::vector &data) = 0; - - void send(uint8_t function, uint16_t start_address, uint16_t register_count) { - this->parent_->send(this->address_, function, start_address, register_count); + virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} + void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, + const uint8_t *payload = nullptr) { + this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } + void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } + // If more than one device is connected block sending a new command before a response is received + bool waiting_for_response() { return parent_->waiting_for_response != 0; } protected: friend Modbus; diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py new file mode 100644 index 0000000000..7a69029dab --- /dev/null +++ b/esphome/components/modbus_controller/__init__.py @@ -0,0 +1,114 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import modbus +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.cpp_helpers import logging +from .const import ( + CONF_COMMAND_THROTTLE, +) + +CODEOWNERS = ["@martgras"] + +AUTO_LOAD = ["modbus"] + +MULTI_CONF = True + +# pylint: disable=invalid-name +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") + +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_REGISTER_TYPE = { + "coil": ModbusRegisterType.COIL, + "discrete_input": ModbusRegisterType.DISCRETE, + "holding": ModbusRegisterType.HOLDING, + "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_QWORDU_R": SensorValueType.U_QWORD_R, + "S_QWORD": SensorValueType.S_QWORD, + "U_QWORD_R": SensorValueType.S_QWORD_R, + "FP32": SensorValueType.FP32, + "FP32_R": SensorValueType.FP32_R, +} + + +MULTI_CONF = True + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ModbusController), + cv.Optional( + CONF_COMMAND_THROTTLE, default="0ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_COMMAND_THROTTLE]) + cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + await register_modbus_device(var, config) + + +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] + + +def find_by_value(dict, find_value): + for (key, value) in MODBUS_REGISTER_TYPE.items(): + print(find_value, value) + if find_value == value: + return key + return "not found" diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py new file mode 100644 index 0000000000..d46ff71f2d --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -0,0 +1,81 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + 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.extend( + { + cv.GenerateID(): cv.declare_id(ModbusBinarySensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + 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)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusBinarySensor.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_template(template_)) 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 0000000000..81066b3f5c --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -0,0 +1,40 @@ +#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: + value = coil_from_vector(this->offset, data); + break; + 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 0000000000..c516d6b916 --- /dev/null +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +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, + uint8_t skip_updates, bool force_new_range) + : Component(), binary_sensor::BinarySensor() { + 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 = + optional(ModbusBinarySensor *, bool, const std::vector &)>>; + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + + protected: + transform_func_t 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 0000000000..3cd114e673 --- /dev/null +++ b/esphome/components/modbus_controller/const.py @@ -0,0 +1,13 @@ +CONF_BITMASK = "bitmask" +CONF_BYTE_OFFSET = "byte_offset" +CONF_COMMAND_THROTTLE = "command_throttle" +CONF_FORCE_NEW_RANGE = "force_new_range" +CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" +CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" +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_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 0000000000..70b5bf8eae --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -0,0 +1,559 @@ +#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() { + // Modbus::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() && !command_queue_.empty()) { + auto &command = command_queue_.front(); + + 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(); + if (!command->on_data_func) { // No handler remove from queue directly after sending + command_queue_.pop_front(); + } + } + return (!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) { + // 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"); + 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 adddress = 0x%X " + "registers count=%d " + "payload size=%zu", + function_code, current_command->register_address, current_command->register_count, + current_command->payload.size()); + command_queue_.pop_front(); + } +} + +void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address); + + auto vec_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) { + return (r.start_address == start_address && r.register_type == register_type); + }); + + if (vec_it == register_ranges_.end()) { + ESP_LOGE(TAG, "Handle incoming data : No matching range for sensor found - start_address : 0x%X", start_address); + return; + } + auto map_it = sensormap_.find(vec_it->first_sensorkey); + if (map_it == sensormap_.end()) { + ESP_LOGE(TAG, "Handle incoming data : No sensor found in at start_address : 0x%X (0x%llX)", start_address, + vec_it->first_sensorkey); + return; + } + // loop through all sensors with the same start address + while (map_it != sensormap_.end() && map_it->second->start_address == start_address) { + if (map_it->second->register_type == register_type) { + map_it->second->parse_and_publish(data); + } + map_it++; + } +} + +void ModbusController::queue_command(const ModbusCommandItem &command) { + // check if this commmand is already qeued. + // not very effective but the queue is never really large + for (auto &item : command_queue_) { + if (item->register_address == command.register_address && item->register_count == command.register_count && + item->register_type == command.register_type) { + ESP_LOGW(TAG, "Duplicate modbus command found"); + // update the payload of the queued command + // replaces a previous command + item->payload = command.payload; + return; + } + } + 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) { + ModbusCommandItem command_item = + ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count); + queue_command(command_item); + 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 (!command_queue_.empty()) { + ESP_LOGV(TAG, "%zu modbus commands already in queue", 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 registerranges to read +size_t ModbusController::create_register_ranges_() { + register_ranges_.clear(); + uint8_t n = 0; + if (sensormap_.empty()) { + return 0; + } + + auto ix = sensormap_.begin(); + auto prev = ix; + int total_register_count = 0; + uint16_t current_start_address = ix->second->start_address; + uint8_t buffer_offset = ix->second->offset; + uint8_t skip_updates = ix->second->skip_updates; + auto first_sensorkey = ix->second->getkey(); + total_register_count = 0; + while (ix != sensormap_.end()) { + ESP_LOGV(TAG, "Register: 0x%X %d %d 0x%llx (%d) buffer_offset = %d (0x%X) skip=%u", ix->second->start_address, + ix->second->register_count, ix->second->offset, ix->second->getkey(), total_register_count, buffer_offset, + buffer_offset, ix->second->skip_updates); + // if this is a sequential address based on number of registers and address of previous sensor + // convert to an offset to the previous sensor (address 0x101 becomes address 0x100 offset 2 bytes) + if (!ix->second->force_new_range && total_register_count >= 0 && + prev->second->register_type == ix->second->register_type && + prev->second->start_address + total_register_count == ix->second->start_address && + prev->second->start_address < ix->second->start_address) { + ix->second->start_address = prev->second->start_address; + ix->second->offset += prev->second->offset + prev->second->get_register_size(); + + // replace entry in sensormap_ + auto const value = ix->second; + sensormap_.erase(ix); + sensormap_.insert({value->getkey(), value}); + // move iterator back to new element + ix = sensormap_.find(value->getkey()); // next(prev, 1); + } + if (current_start_address != ix->second->start_address || + // ( prev->second->start_address + prev->second->offset != ix->second->start_address) || + ix->second->register_type != prev->second->register_type) { + // Difference doesn't match so we have a gap + if (n > 0) { + RegisterRange r; + r.start_address = current_start_address; + r.register_count = total_register_count; + if (prev->second->register_type == ModbusRegisterType::COIL || + prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { + r.register_count = prev->second->offset + 1; + } + r.register_type = prev->second->register_type; + r.first_sensorkey = first_sensorkey; + r.skip_updates = skip_updates; + r.skip_updates_counter = 0; + ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + } + skip_updates = ix->second->skip_updates; + current_start_address = ix->second->start_address; + first_sensorkey = ix->second->getkey(); + total_register_count = ix->second->register_count; + buffer_offset = ix->second->offset; + n = 1; + } else { + n++; + if (ix->second->offset != prev->second->offset || n == 1) { + total_register_count += ix->second->register_count; + buffer_offset += ix->second->get_register_size(); + } + // 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 (ix->second->skip_updates != 0) { + if (skip_updates != 0) { + skip_updates = std::min(skip_updates, ix->second->skip_updates); + } else { + skip_updates = ix->second->skip_updates; + } + } + } + prev = ix++; + } + // Add the last range + if (n > 0) { + RegisterRange r; + r.start_address = current_start_address; + // r.register_count = prev->second->offset>>1 + prev->second->get_register_size(); + r.register_count = total_register_count; + if (prev->second->register_type == ModbusRegisterType::COIL || + prev->second->register_type == ModbusRegisterType::DISCRETE_INPUT) { + r.register_count = prev->second->offset + 1; + } + r.register_type = prev->second->register_type; + r.first_sensorkey = first_sensorkey; + r.skip_updates = skip_updates; + r.skip_updates_counter = 0; + ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates); + register_ranges_.push_back(r); + } + return register_ranges_.size(); +} + +void ModbusController::dump_config() { + ESP_LOGCONFIG(TAG, "ModbusController:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGCONFIG(TAG, "sensormap"); + for (auto &it : sensormap_) { + ESP_LOGCONFIG("TAG", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), it.second->start_address, + it.second->register_count, it.second->get_register_size()); + } +#endif +} + +void ModbusController::loop() { + // Incoming data to process? + if (!incoming_queue_.empty()) { + auto &message = incoming_queue_.front(); + if (message != nullptr) + process_modbus_data_(message.get()); + incoming_queue_.pop(); + + } else { + // all messages processed send pending commmands + 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_sensormap_() { + ESP_LOGV("modbuscontroller.h", "sensormap"); + for (auto &it : sensormap_) { + ESP_LOGV("modbuscontroller.h", " Sensor 0x%llX start=0x%X count=%d size=%d", it.second->getkey(), + it.second->start_address, it.second->register_count, it.second->get_register_size()); + } +} + +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) { + cmd.payload.push_back((v / 256) & 0xFF); + cmd.payload.push_back(v & 0xFF); + } + 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, + int16_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); + }; + cmd.payload.push_back((value / 256) & 0xFF); + cmd.payload.push_back((value % 256) & 0xFF); + 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, uint16_t, const std::vector &data) { + ESP_LOGI(TAG, "Custom Command sent"); + }; + } else { + cmd.on_data_func = handler; + } + cmd.payload = values; + + 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); + } + ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count); + return true; +} + +std::vector float_to_payload(float value, SensorValueType value_type) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + std::vector data; + int32_t val; + + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + // cast truncates the float do some rounding here + data.push_back(lroundf(value) & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + val = lroundf(value); + data.push_back((val & 0xFFFF0000) >> 16); + data.push_back(val & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + val = lroundf(value); + data.push_back(val & 0xFFFF); + data.push_back((val & 0xFFFF0000) >> 16); + break; + case SensorValueType::FP32: + raw_to_float.float_value = value; + data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); + data.push_back(raw_to_float.raw & 0xFFFF); + break; + case SensorValueType::FP32_R: + raw_to_float.float_value = value; + data.push_back(raw_to_float.raw & 0xFFFF); + data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); + break; + default: + ESP_LOGE(TAG, "Invalid data type for modbus float to payload conversation"); + break; + } + return data; +} + +float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits + float result = NAN; + + switch (sensor_value_type) { + case SensorValueType::U_WORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; + result = static_cast(value); + break; + case SensorValueType::U_DWORD: + value = get_data(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + result = static_cast(value); + break; + case SensorValueType::U_DWORD_R: + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + result = static_cast(value); + break; + case SensorValueType::S_WORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), + bitmask); // default is 0xFFFF ; + result = static_cast(value); + break; + case SensorValueType::S_DWORD: + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); + result = static_cast(value); + break; + case SensorValueType::S_DWORD_R: { + 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); + result = static_cast(value); + } break; + case SensorValueType::U_QWORD: + // Ignore bitmask for U_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + + case SensorValueType::S_QWORD: + // Ignore bitmask for S_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + case SensorValueType::U_QWORD_R: + // Ignore bitmask for U_QWORD + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | + static_cast(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | + static_cast(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; + result = static_cast(value); + break; + + case SensorValueType::S_QWORD_R: + // Ignore bitmask for S_QWORD + value = get_data(data, offset); + result = static_cast(value); + break; + case SensorValueType::FP32: + raw_to_float.raw = get_data(data, offset); + ESP_LOGD(TAG, "FP32 = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); + result = raw_to_float.float_value; + break; + case SensorValueType::FP32_R: { + auto tmp = get_data(data, offset); + raw_to_float.raw = static_cast(tmp & 0xFFFF) << 16 | (tmp & 0xFFFF0000) >> 16; + ESP_LOGD(TAG, "FP32_R = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); + result = raw_to_float.float_value; + } break; + default: + break; + } + return result; +} + +} // 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 0000000000..4b5f4337db --- /dev/null +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -0,0 +1,454 @@ +#pragma once + +#include "esphome/core/component.h" + +#include "esphome/core/automation.h" +#include "esphome/components/modbus/modbus.h" + +#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 : int { + 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 +}; + +struct RegisterRange { + uint16_t start_address; + ModbusRegisterType register_type; + uint8_t register_count; + uint8_t skip_updates; // the config value + uint64_t first_sensorkey; + uint8_t skip_updates_counter; // the running value +} __attribute__((packed)); + +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: + return ModbusFunctionCode::CUSTOM; + break; + default: + return ModbusFunctionCode::CUSTOM; + break; + } +} + +/** All sensors are stored in a map + * to enable binary sensors for values encoded as bits in the same register the key of each sensor + * the key is a 64 bit integer that combines the register properties + * sensormap_ is sorted by this key. The key ensures the correct order when creating consequtive ranges + * Format: function_code (8 bit) | start address (16 bit)| offset (8bit)| bitmask (32 bit) + */ +inline uint64_t calc_key(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset = 0, + uint32_t bitmask = 0) { + return uint64_t((uint16_t(register_type) << 24) + (uint32_t(start_address) << 8) + (offset & 0xFF)) << 32 | bitmask; +} +inline uint16_t register_from_key(uint64_t key) { return (key >> 40) & 0xFFFF; } + +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 postion if the first right set bit in the mask + * Usefull 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) { + return result; + } + for (int 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 value float value to cconvert + * @param value_type defines if 16/32 or FP32 is used + * @return vector containing the modbus register words in correct order + */ +std::vector float_to_payload(float value, SensorValueType value_type); + +/** convert vector response payload to float + * @param value float value to cconvert + * @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 float version of the input + */ +float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); + +class ModbusController; + +struct SensorItem { + ModbusRegisterType register_type; + SensorValueType sensor_value_type; + uint16_t start_address; + uint32_t bitmask; + uint8_t offset; + uint8_t register_count; + uint8_t skip_updates; + bool force_new_range{false}; + + virtual void parse_and_publish(const std::vector &data) = 0; + + uint64_t getkey() const { return calc_key(register_type, start_address, offset, bitmask); } + + size_t virtual get_register_size() const { + size_t size = 0; + switch (sensor_value_type) { + case SensorValueType::BIT: + size = 1; + break; + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + size = 2; + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32: + case SensorValueType::FP32_R: + size = 4; + break; + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + size = 8; + break; + case SensorValueType::RAW: + size = this->register_count * 2; + } + return size; + } +}; + +struct ModbusCommandItem { + static const size_t MAX_PAYLOAD_BYTES = 240; + ModbusController *modbusdevice; + uint16_t register_address; + uint16_t register_count; + ModbusFunctionCode function_code; + ModbusRegisterType register_type; + std::function &data)> + on_data_func; + std::vector payload = {}; + bool send(); + + /// 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 values uint16_t array to be written to the registers + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, + int16_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 compplete payload must be provided with the + * exception of the crc codess + * @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); +}; + +/** 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: + ModbusController(uint16_t throttle = 0) : modbus::ModbusDevice(), command_throttle_(throttle){}; + 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) { sensormap_[item->getkey()] = item; } + /// called when a modbus response was prased 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; + /// 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); + /// called by esphome generated code to set the command_throttle period + void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; } + + protected: + /// parse sensormap_ and create range of sequential addresses + size_t create_register_ranges_(); + /// 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_(); + /// get the number of queued modbus commands (should be mostly empty) + size_t get_command_queue_length_() { return command_queue_.size(); } + /// dump the parsed sensormap for diagnostics + void dump_sensormap_(); + /// Collection of all sensors for this component + /// see calc_key how the key is contructed + std::map sensormap_; + /// Continous 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_; + /// when was the last send operation + uint32_t last_command_timestamp_; + /// min time in ms between sending modbus commands + uint16_t command_throttle_; +}; + +/** convert vector response payload to float + * @param value float value to cconvert + * @param item SensorItem object + * @return float version of the input + */ +inline float payload_to_float(const std::vector &data, const SensorItem &item) { + return payload_to_float(data, item.sensor_value_type, item.offset, item.bitmask); +} + +} // 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 0000000000..c7919bb972 --- /dev/null +++ b/esphome/components/modbus_controller/number/__init__.py @@ -0,0 +1,157 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_LAMBDA, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MULTIPLY, + CONF_OFFSET, + CONF_STEP, +) + +from .. import ( + modbus_controller_ns, + ModbusController, + SENSOR_VALUE_TYPE, + SensorItem, +) + + +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_SKIP_UPDATES, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusNumber = modbus_controller_ns.class_( + "ModbusNumber", cg.Component, number.Number, SensorItem +) + +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_QWORDU_R": 4, + "S_QWORD": 4, + "U_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + + +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 + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusNumber), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.GenerateID(): cv.declare_id(ModbusNumber), + # 24 bits are the maximum value for fp32 before precison is lost + # 0x00FFFFFF = 16777215 + cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_, + 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_, + } + ).extend(cv.polling_component_schema("60s")), + validate_min_max, +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + value_type = config[CONF_VALUE_TYPE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + var = cg.new_Pvariable( + config[CONF_ID], + 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)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusNumber.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_template(template_)) + 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 0000000000..95c6ac6f6a --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -0,0 +1,83 @@ +#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) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + 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, "Number new state : %.02f", result); + // this->sensor_->raw_state = result; + this->publish_state(result); +} + +void ModbusNumber::control(float value) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + 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 = 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: 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, value); + + // Create and send the write command + auto write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + 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 + parent_->on_write_register_response(write_cmd.register_type, start_address, data); + this->publish_state(value); + }; + parent_->queue_command(write_cmd); +} +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 0000000000..0fd4e314bc --- /dev/null +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +using value_to_data_t = std::function(float); + +class ModbusNumber : public number::Number, public Component, public SensorItem { + public: + ModbusNumber(uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, + uint8_t skip_updates, bool force_new_range) + : number::Number(), Component(), SensorItem() { + this->register_type = ModbusRegisterType::HOLDING; + 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_update_interval(int) {} + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { 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; } + + protected: + void control(float value) override; + optional transform_func_; + optional write_transform_func_; + ModbusController *parent_; + float multiply_by_{1.0}; +}; + +} // 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 0000000000..9c41fc011c --- /dev/null +++ b/esphome/components/modbus_controller/output/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output + +from esphome.const import ( + CONF_ADDRESS, + CONF_ID, + CONF_MULTIPLY, + CONF_OFFSET, +) + +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, +) + +from ..const import ( + CONF_BYTE_OFFSET, + CONF_MODBUS_CONTROLLER_ID, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) +from ..sensor import SENSOR_VALUE_TYPE + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusOutput = modbus_controller_ns.class_( + "ModbusOutput", cg.Component, output.FloatOutput, SensorItem +) + +CONFIG_SCHEMA = cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.GenerateID(): cv.declare_id(ModbusOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + } + ), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + var = cg.new_Pvariable( + config[CONF_ID], config[CONF_ADDRESS], byte_offset, config[CONF_VALUE_TYPE] + ) + await output.register_output(var, config) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(var.set_parent(parent)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusOutput.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp new file mode 100644 index 0000000000..f7d7c42342 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -0,0 +1,61 @@ +#include +#include "modbus_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.output"; + +void ModbusOutput::setup() {} + +/** Write a value to the device + * + */ +void ModbusOutput::write_state(float value) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + 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 = 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 + auto write_cmd = + ModbusCommandItem::create_write_multiple_command(parent_, this->start_address, this->register_count, data); + parent_->queue_command(write_cmd); +} + +void ModbusOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus Float Output:"); + LOG_FLOAT_OUTPUT(this); + ESP_LOGCONFIG(TAG, "Modbus device start address=0x%X register count=%d value type=%hhu", this->start_address, + this->register_count, this->sensor_value_type); +} + +} // 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 0000000000..f46aef4683 --- /dev/null +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +using value_to_data_t = std::function(float); + +class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { + public: + ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type) + : output::FloatOutput(), Component() { + this->register_type = ModbusRegisterType::HOLDING; + this->start_address = start_address; + this->offset = offset; + this->bitmask = bitmask; + this->sensor_value_type = value_type; + this->skip_updates = 0; + this->start_address += offset; + this->offset = 0; + } + void setup() override; + void dump_config() override; + + void set_parent(ModbusController *parent) { this->parent_ = parent; } + void set_write_multiply(float factor) { multiply_by_ = factor; } + // Do nothing + void parse_and_publish(const std::vector &data) override{}; + + using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + + protected: + void write_state(float value) override; + optional write_transform_func_{nullopt}; + + ModbusController *parent_; + float multiply_by_{1.0}; +}; + +} // 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 0000000000..687f3d82fb --- /dev/null +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -0,0 +1,109 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + 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 +) + +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_QWORDU_R": 4, + "S_QWORD": 4, + "U_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + + +CONFIG_SCHEMA = cv.All( + sensor.SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusSensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + value_type = config[CONF_VALUE_TYPE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] + 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 sensor.register_sensor(var, config) + + paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(paren.add_sensor_item(var)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSensor.operator("ptr"), "item"), + (cg.float_, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_template(template_)) 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 0000000000..dbd0525347 --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -0,0 +1,36 @@ + +#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) { + union { + float float_value; + uint32_t raw; + } raw_to_float; + + 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 0000000000..4f48c2a4dd --- /dev/null +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +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, uint8_t skip_updates, bool force_new_range) + : Component(), sensor::Sensor() { + 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 0000000000..e03b0d37be --- /dev/null +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -0,0 +1,81 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + MODBUS_REGISTER_TYPE, + SensorItem, + modbus_controller_ns, + ModbusController, +) +from ..const import ( + CONF_BITMASK, + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras"] + + +ModbusSwitch = modbus_controller_ns.class_( + "ModbusSwitch", cg.Component, switch.Switch, SensorItem +) + + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusSwitch), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_BITMASK, default=0x1): cv.hex_uint32_t, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_REGISTER_TYPE], + config[CONF_ADDRESS], + byte_offset, + config[CONF_BITMASK], + 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(paren.add_sensor_item(var)) + cg.add(var.set_parent(paren)) + if CONF_LAMBDA in config: + publish_template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSwitch.operator("ptr"), "item"), + (bool, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(bool), + ) + cg.add(var.set_template(publish_template_)) 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 0000000000..ce9557e6c4 --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -0,0 +1,70 @@ + +#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() { + // value isn't required + // without it we crash on save + this->get_initial_state(); +} +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; + ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), + ONOFF(state), (int) this->register_type, this->start_address, this->offset); + switch (this->register_type) { + case ModbusRegisterType::COIL: + // offset for coil and discrete inputs is the coil/register number not bytes + cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state); + break; + case ModbusRegisterType::DISCRETE_INPUT: + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, state); + break; + + default: + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, + state ? 0xFFFF & this->bitmask : 0); + break; + } + this->parent_->queue_command(cmd); + 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 0000000000..a38668fabb --- /dev/null +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +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, + bool force_new_range) + : Component(), switch_::Switch() { + 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 = 0; + 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 &)>; + void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } + + protected: + ModbusController *parent_; + optional publish_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 0000000000..2c02c86795 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -0,0 +1,101 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg + + +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_LAMBDA, CONF_OFFSET +from .. import ( + SensorItem, + modbus_controller_ns, + ModbusController, + MODBUS_REGISTER_TYPE, +) +from ..const import ( + CONF_BYTE_OFFSET, + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_RESPONSE_SIZE, + CONF_SKIP_UPDATES, + CONF_RAW_ENCODE, + CONF_REGISTER_TYPE, +) + +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, +} + +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ModbusTextSensor), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_OFFSET, default=0): cv.positive_int, + cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, + cv.Optional(CONF_RAW_ENCODE, default="NONE"): cv.enum(RAW_ENCODING), + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ).extend(cv.COMPONENT_SCHEMA), +) + + +async def to_code(config): + byte_offset = 0 + if CONF_OFFSET in config: + byte_offset = config[CONF_OFFSET] + # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET + if CONF_BYTE_OFFSET in config: + byte_offset = config[CONF_BYTE_OFFSET] + 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)) + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusTextSensor.operator("ptr"), "item"), + (cg.std_string.operator("const").operator("ref"), "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(cg.std_string), + ) + cg.add(var.set_template(template_)) 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 0000000000..a06d44e90b --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp @@ -0,0 +1,56 @@ + +#include "modbus_textsensor.h" +#include "esphome/core/log.h" +#include +#include + +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::ostringstream output; + uint8_t max_items = this->response_bytes_; + char buffer[4]; + bool add_comma = false; + for (auto b : data) { + switch (this->encode_) { + case RawEncoding::HEXBYTES: + sprintf(buffer, "%02x", b); + output << buffer; + break; + case RawEncoding::COMMA: + sprintf(buffer, add_comma ? ",%d" : "%d", b); + output << buffer; + add_comma = true; + break; + // Anything else no encoding + case RawEncoding::NONE: + default: + output << (char) b; + break; + } + if (--max_items == 0) { + break; + } + } + + auto result = output.str(); + // 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(); + } + } + this->publish_state(result); +} + +} // 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 0000000000..28d0f0b241 --- /dev/null +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2 }; + +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, uint8_t skip_updates, bool force_new_range) + : Component() { + 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; + } + size_t get_register_size() const override { + if (sensor_value_type == SensorValueType::RAW) { + return this->response_bytes_; + } else { + return SensorItem::get_register_size(); + } + } + + 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}; + + protected: + RawEncoding encode_; + uint16_t response_bytes_; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index f165617551..b22b19550e 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -36,6 +36,14 @@ i2c: modbus: uart_id: uart1 + flow_control_pin: 5 + id: mod_bus1 + +modbus_controller: + - id: modbus_controller_test + address: 0x2 + modbus_id: mod_bus1 + binary_sensor: - platform: gpio @@ -150,6 +158,14 @@ sensor: name: "SelecEM2M Maximum Demand Apparent Power" disabled_by_default: true + - id: battery_voltage + name: "Battery voltage2" + platform: modbus_controller + modbus_controller_id: modbus_controller_test + address: 0x331A + register_type: read + value_type: U_WORD + - platform: t6615 uart_id: uart2 co2: