mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01:00
Modbus controller (#1779)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
4d28afc153
commit
7672ba2c8d
27 changed files with 2505 additions and 34 deletions
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
114
esphome/components/modbus_controller/__init__.py
Normal file
114
esphome/components/modbus_controller/__init__.py
Normal 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"
|
|
@ -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_))
|
|
@ -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
|
|
@ -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
|
13
esphome/components/modbus_controller/const.py
Normal file
13
esphome/components/modbus_controller/const.py
Normal 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"
|
559
esphome/components/modbus_controller/modbus_controller.cpp
Normal file
559
esphome/components/modbus_controller/modbus_controller.cpp
Normal 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 ¤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<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
|
454
esphome/components/modbus_controller/modbus_controller.h
Normal file
454
esphome/components/modbus_controller/modbus_controller.h
Normal 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
|
157
esphome/components/modbus_controller/number/__init__.py
Normal file
157
esphome/components/modbus_controller/number/__init__.py
Normal 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_))
|
|
@ -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
|
48
esphome/components/modbus_controller/number/modbus_number.h
Normal file
48
esphome/components/modbus_controller/number/modbus_number.h
Normal 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
|
74
esphome/components/modbus_controller/output/__init__.py
Normal file
74
esphome/components/modbus_controller/output/__init__.py
Normal 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_))
|
|
@ -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
|
45
esphome/components/modbus_controller/output/modbus_output.h
Normal file
45
esphome/components/modbus_controller/output/modbus_output.h
Normal 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
|
109
esphome/components/modbus_controller/sensor/__init__.py
Normal file
109
esphome/components/modbus_controller/sensor/__init__.py
Normal 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_))
|
|
@ -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
|
35
esphome/components/modbus_controller/sensor/modbus_sensor.h
Normal file
35
esphome/components/modbus_controller/sensor/modbus_sensor.h
Normal 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
|
81
esphome/components/modbus_controller/switch/__init__.py
Normal file
81
esphome/components/modbus_controller/switch/__init__.py
Normal 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_))
|
|
@ -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
|
44
esphome/components/modbus_controller/switch/modbus_switch.h
Normal file
44
esphome/components/modbus_controller/switch/modbus_switch.h
Normal 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
|
101
esphome/components/modbus_controller/text_sensor/__init__.py
Normal file
101
esphome/components/modbus_controller/text_sensor/__init__.py
Normal 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_))
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue