Modbus controller (#1779)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Martin 2021-09-26 22:27:24 +02:00 committed by GitHub
parent 4d28afc153
commit 7672ba2c8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 2505 additions and 34 deletions

View file

@ -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

View file

@ -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 = {

View file

@ -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<uint8_t> data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len);
waiting_for_response = 0;
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> &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

View file

@ -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<uint8_t> &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<uint8_t> rx_buffer_;
uint32_t last_modbus_byte_{0};
uint32_t last_send_{0};
std::vector<ModbusDevice *> 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<uint8_t> &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<uint8_t> &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;

View file

@ -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"

View file

@ -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_))

View file

@ -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<uint8_t> &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<uint16_t>(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

View file

@ -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<uint8_t> &data) override;
void set_state(bool state) { this->state = state; }
void dump_config() override;
using transform_func_t =
optional<std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected:
transform_func_t transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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"

View file

@ -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<uint8_t> &data) {
auto &current_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 &current_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<uint8_t> &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<ModbusCommandItem>(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<uint8_t> &data) {
ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data<uint16_t>(data, 0), get_data<int16_t>(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<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &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<uint8_t> &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<uint16_t> &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<uint8_t> &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<uint8_t> &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<bool> &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<uint8_t> &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<uint8_t> &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<uint8_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &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<uint8_t> &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<uint16_t> float_to_payload(float value, SensorValueType value_type) {
union {
float float_value;
uint32_t raw;
} raw_to_float;
std::vector<uint16_t> 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<uint8_t> &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<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
result = static_cast<float>(value);
break;
case SensorValueType::U_DWORD:
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::U_DWORD_R:
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::S_WORD:
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
result = static_cast<float>(value);
break;
case SensorValueType::S_DWORD:
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::S_DWORD_R: {
value = get_data<uint32_t>(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<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
result = static_cast<float>(value);
} break;
case SensorValueType::U_QWORD:
// Ignore bitmask for U_QWORD
value = get_data<uint64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::S_QWORD:
// Ignore bitmask for S_QWORD
value = get_data<int64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::U_QWORD_R:
// Ignore bitmask for U_QWORD
value = get_data<uint64_t>(data, offset);
value = static_cast<uint64_t>(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 |
static_cast<uint64_t>(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 |
static_cast<uint64_t>(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16;
result = static_cast<float>(value);
break;
case SensorValueType::S_QWORD_R:
// Ignore bitmask for S_QWORD
value = get_data<int64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::FP32:
raw_to_float.raw = get_data<uint32_t>(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<uint32_t>(data, offset);
raw_to_float.raw = static_cast<uint32_t>(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

View file

@ -0,0 +1,454 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/modbus/modbus.h"
#include <list>
#include <map>
#include <queue>
#include <vector>
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<uint64_t>(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<typename T> T get_data(const std::vector<uint8_t> &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<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2));
}
if (sizeof(T) == sizeof(uint64_t)) {
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
(static_cast<uint64_t>(get_data<uint32_t>(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<uint8_t> &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<typename N> 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<uint16_t> 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<uint16_t> float_to_payload(float value, SensorValueType value_type);
/** convert vector<uint8_t> 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<uint8_t> &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<uint8_t> &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<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
on_data_func;
std::vector<uint8_t> 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<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &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<uint16_t> &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<bool> &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<uint8_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint64_t, SensorItem *> sensormap_;
/// Continous range of modbus registers
std::vector<RegisterRange> register_ranges_;
/// Hold the pending requests to be sent
std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
/// modbus response data waiting to get processed
std::queue<std::unique_ptr<ModbusCommandItem>> 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<uint8_t> 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<uint8_t> &data, const SensorItem &item) {
return payload_to_float(data, item.sensor_value_type, item.offset, item.bitmask);
}
} // namespace modbus_controller
} // namespace esphome

View file

@ -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_))

View file

@ -0,0 +1,83 @@
#include <vector>
#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<uint8_t> &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<uint16_t> 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<uint8_t> &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

View file

@ -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>(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<uint8_t> &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<optional<float>(ModbusNumber *, float, const std::vector<uint8_t> &)>;
using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_t> &)>;
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_t> transform_func_;
optional<write_transform_func_t> write_transform_func_;
ModbusController *parent_;
float multiply_by_{1.0};
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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_))

View file

@ -0,0 +1,61 @@
#include <vector>
#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<uint16_t> 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

View file

@ -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>(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<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<float>(ModbusOutput *, float, std::vector<uint16_t> &)>;
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_t> write_transform_func_{nullopt};
ModbusController *parent_;
float multiply_by_{1.0};
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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_))

View file

@ -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<uint8_t> &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

View file

@ -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<uint8_t> &data) override;
void dump_config() override;
using transform_func_t = std::function<optional<float>(ModbusSensor *, float, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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_))

View file

@ -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<uint8_t> &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<uint16_t>(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

View file

@ -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<uint8_t> &data) override;
void set_parent(ModbusController *parent) { this->parent_ = parent; }
using transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; }
protected:
ModbusController *parent_;
optional<transform_func_t> publish_transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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_))

View file

@ -0,0 +1,56 @@
#include "modbus_textsensor.h"
#include "esphome/core/log.h"
#include <iomanip>
#include <sstream>
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<uint8_t> &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

View file

@ -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<uint8_t> &data) override;
using transform_func_t =
std::function<optional<std::string>(ModbusTextSensor *, std::string, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};
protected:
RawEncoding encode_;
uint16_t response_bytes_;
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -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: