Shove modbus_controller stuff to see if I can make it compile

This commit is contained in:
Martin Kennedy 2025-03-26 23:11:52 -04:00
parent 074e447046
commit 24da1279d8
26 changed files with 3201 additions and 0 deletions

View File

@ -0,0 +1,338 @@
import binascii
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_LAMBDA,
CONF_NAME,
CONF_OFFSET,
CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging
from .const import (
CONF_ALLOW_DUPLICATE_COMMANDS,
CONF_BITMASK,
CONF_BYTE_OFFSET,
CONF_COMMAND_THROTTLE,
CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE,
CONF_MAX_CMD_RETRIES,
CONF_MODBUS_CONTROLLER_ID,
CONF_OFFLINE_SKIP_UPDATES,
CONF_ON_COMMAND_SENT,
CONF_ON_ONLINE,
CONF_ON_OFFLINE,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE,
CONF_SKIP_UPDATES,
CONF_VALUE_TYPE,
)
CODEOWNERS = ["@martgras"]
AUTO_LOAD = ["modbus"]
CONF_READ_LAMBDA = "read_lambda"
CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
ModbusController = modbus_controller_ns.class_(
"ModbusController", cg.PollingComponent, modbus.ModbusDevice
)
SensorItem = modbus_controller_ns.struct("SensorItem")
ServerRegister = modbus_controller_ns.struct("ServerRegister")
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
MODBUS_FUNCTION_CODE = {
"read_coils": ModbusFunctionCode.READ_COILS,
"read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS,
"read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS,
"read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS,
"write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL,
"write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER,
"write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS,
"write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS,
}
ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType")
ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType")
MODBUS_WRITE_REGISTER_TYPE = {
"custom": ModbusRegisterType.CUSTOM,
"coil": ModbusRegisterType.COIL,
"holding": ModbusRegisterType.HOLDING,
}
MODBUS_REGISTER_TYPE = {
**MODBUS_WRITE_REGISTER_TYPE,
"discrete_input": ModbusRegisterType.DISCRETE_INPUT,
"read": ModbusRegisterType.READ,
}
SensorValueType_ns = modbus_controller_ns.namespace("SensorValueType")
SensorValueType = SensorValueType_ns.enum("SensorValueType")
SENSOR_VALUE_TYPE = {
"RAW": SensorValueType.RAW,
"U_WORD": SensorValueType.U_WORD,
"S_WORD": SensorValueType.S_WORD,
"U_DWORD": SensorValueType.U_DWORD,
"U_DWORD_R": SensorValueType.U_DWORD_R,
"S_DWORD": SensorValueType.S_DWORD,
"S_DWORD_R": SensorValueType.S_DWORD_R,
"U_QWORD": SensorValueType.U_QWORD,
"U_QWORD_R": SensorValueType.U_QWORD_R,
"S_QWORD": SensorValueType.S_QWORD,
"S_QWORD_R": SensorValueType.S_QWORD_R,
"FP32": SensorValueType.FP32,
"FP32_R": SensorValueType.FP32_R,
}
TYPE_REGISTER_MAP = {
"RAW": 1,
"U_WORD": 1,
"S_WORD": 1,
"U_DWORD": 2,
"U_DWORD_R": 2,
"S_DWORD": 2,
"S_DWORD_R": 2,
"U_QWORD": 4,
"U_QWORD_R": 4,
"S_QWORD": 4,
"S_QWORD_R": 4,
"FP32": 2,
"FP32_R": 2,
}
ModbusCommandSentTrigger = modbus_controller_ns.class_(
"ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
ModbusOnlineTrigger = modbus_controller_ns.class_(
"ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
ModbusOfflineTrigger = modbus_controller_ns.class_(
"ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
_LOGGER = logging.getLogger(__name__)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ModbusController),
cv.Optional(CONF_ALLOW_DUPLICATE_COMMANDS, default=False): cv.boolean,
cv.Optional(
CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(
CONF_SERVER_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ModbusCommandSentTrigger
),
}
),
cv.Optional(CONF_ON_ONLINE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
}
),
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(modbus.modbus_device_schema(0x01))
)
ModbusItemBaseSchema = cv.Schema(
{
cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController),
cv.Optional(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_CUSTOM_COMMAND): cv.ensure_list(cv.hex_uint8_t),
cv.Exclusive(
CONF_OFFSET,
"offset",
f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together",
): cv.positive_int,
cv.Exclusive(
CONF_BYTE_OFFSET,
"offset",
f"{CONF_OFFSET} and {CONF_BYTE_OFFSET} can't be used together",
): cv.positive_int,
cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t,
cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_RESPONSE_SIZE, default=0): cv.positive_int,
},
)
def validate_modbus_register(config):
if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config:
raise cv.Invalid(
f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used"
)
if CONF_CUSTOM_COMMAND in config and CONF_REGISTER_TYPE in config:
raise cv.Invalid(
f"can't use '{CONF_REGISTER_TYPE}:' together with '{CONF_CUSTOM_COMMAND}:'",
)
if CONF_CUSTOM_COMMAND not in config and CONF_REGISTER_TYPE not in config:
raise cv.Invalid(
f" {CONF_REGISTER_TYPE} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used"
)
return config
def _final_validate(config):
if CONF_SERVER_REGISTERS in config:
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
config
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def modbus_calc_properties(config):
byte_offset = 0
reg_count = 0
if CONF_OFFSET in config:
byte_offset = config[CONF_OFFSET]
# A CONF_BYTE_OFFSET setting overrides CONF_OFFSET
if CONF_BYTE_OFFSET in config:
byte_offset = config[CONF_BYTE_OFFSET]
if CONF_REGISTER_COUNT in config:
reg_count = config[CONF_REGISTER_COUNT]
if CONF_VALUE_TYPE in config:
value_type = config[CONF_VALUE_TYPE]
if reg_count == 0:
reg_count = TYPE_REGISTER_MAP[value_type]
if CONF_CUSTOM_COMMAND in config:
if CONF_ADDRESS not in config:
# generate a unique modbus address using the hash of the name
# CONF_NAME set even if only CONF_ID is used.
# a modbus register address is required to add the item to sensormap
value = config[CONF_NAME]
if isinstance(value, str):
value = value.encode()
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
config[CONF_FORCE_NEW_RANGE] = True
return byte_offset, reg_count
async def add_modbus_base_properties(
var, config, sensor_type, lambda_param_type=cg.float_, lambda_return_type=float
):
if CONF_CUSTOM_COMMAND in config:
cg.add(var.set_custom_data(config[CONF_CUSTOM_COMMAND]))
if config[CONF_RESPONSE_SIZE] > 0:
cg.add(var.set_register_size(config[CONF_RESPONSE_SIZE]))
if CONF_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_LAMBDA],
[
(sensor_type.operator("ptr"), "item"),
(lambda_param_type, "x"),
(
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
"data",
),
],
return_type=cg.optional.template(lambda_return_type),
)
cg.add(var.set_template(template_))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:
cg.add(
var.add_server_register(
cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[],
return_type=cg.float_,
),
)
)
)
await register_modbus_device(var, config)
for conf in config.get(CONF_ON_COMMAND_SENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
)
for conf in config.get(CONF_ON_ONLINE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
)
for conf in config.get(CONF_ON_OFFLINE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
)
async def register_modbus_device(var, config):
cg.add(var.set_address(config[CONF_ADDRESS]))
await cg.register_component(var, config)
return await modbus.register_modbus_device(var, config)
def function_code_to_register(function_code):
FUNCTION_CODE_TYPE_MAP = {
"read_coils": ModbusRegisterType.COIL,
"read_discrete_inputs": ModbusRegisterType.DISCRETE,
"read_holding_registers": ModbusRegisterType.HOLDING,
"read_input_registers": ModbusRegisterType.READ,
"write_single_coil": ModbusRegisterType.COIL,
"write_single_register": ModbusRegisterType.HOLDING,
"write_multiple_coils": ModbusRegisterType.COIL,
"write_multiple_registers": ModbusRegisterType.HOLDING,
}
return FUNCTION_CODE_TYPE_MAP[function_code]

View File

@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
namespace esphome {
namespace modbus_controller {
class ModbusCommandSentTrigger : public Trigger<int, int> {
public:
ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) {
a_modbuscontroller->add_on_command_sent_callback(
[this](int function_code, int address) { this->trigger(function_code, address); });
}
};
class ModbusOnlineTrigger : public Trigger<int, int> {
public:
ModbusOnlineTrigger(ModbusController *a_modbuscontroller) {
a_modbuscontroller->add_on_online_callback(
[this](int function_code, int address) { this->trigger(function_code, address); });
}
};
class ModbusOfflineTrigger : public Trigger<int, int> {
public:
ModbusOfflineTrigger(ModbusController *a_modbuscontroller) {
a_modbuscontroller->add_on_offline_callback(
[this](int function_code, int address) { this->trigger(function_code, address); });
}
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,60 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
CONF_SKIP_UPDATES,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusBinarySensor = modbus_controller_ns.class_(
"ModbusBinarySensor", cg.Component, binary_sensor.BinarySensor, SensorItem
)
CONFIG_SCHEMA = cv.All(
binary_sensor.binary_sensor_schema(ModbusBinarySensor)
.extend(cv.COMPONENT_SCHEMA)
.extend(ModbusItemBaseSchema)
.extend(
{
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
}
),
validate_modbus_register,
)
async def to_code(config):
byte_offset, _ = modbus_calc_properties(config)
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
config[CONF_ADDRESS],
byte_offset,
config[CONF_BITMASK],
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
)
await cg.register_component(var, config)
await binary_sensor.register_binary_sensor(var, config)
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(paren.add_sensor_item(var))
await add_modbus_base_properties(var, config, ModbusBinarySensor, bool, bool)

View File

@ -0,0 +1,38 @@
#include "modbus_binarysensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.binary_sensor";
void ModbusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Modbus Controller Binary Sensor", this); }
void ModbusBinarySensor::parse_and_publish(const std::vector<uint8_t> &data) {
bool value;
switch (this->register_type) {
case ModbusRegisterType::DISCRETE_INPUT:
case ModbusRegisterType::COIL:
// offset for coil is the actual number of the coil not the byte offset
value = coil_from_vector(this->offset, data);
break;
default:
value = get_data<uint16_t>(data, this->offset) & this->bitmask;
break;
}
// Is there a lambda registered
// call it with the pre converted value and the raw data array
if (this->transform_func_.has_value()) {
// the lambda can parse the response itself
auto val = (*this->transform_func_)(this, value, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
value = val.value();
}
}
this->publish_state(value);
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,44 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace modbus_controller {
class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor, public SensorItem {
public:
ModbusBinarySensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
uint16_t skip_updates, bool force_new_range) {
this->register_type = register_type;
this->start_address = start_address;
this->offset = offset;
this->bitmask = bitmask;
this->sensor_value_type = SensorValueType::BIT;
this->skip_updates = skip_updates;
this->force_new_range = force_new_range;
if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) {
this->register_count = offset + 1;
} else {
this->register_count = 1;
}
}
void parse_and_publish(const std::vector<uint8_t> &data) override;
void set_state(bool state) { this->state = state; }
void dump_config() override;
using transform_func_t = std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,21 @@
CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands"
CONF_BITMASK = "bitmask"
CONF_BYTE_OFFSET = "byte_offset"
CONF_COMMAND_THROTTLE = "command_throttle"
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range"
CONF_MAX_CMD_RETRIES = "max_cmd_retries"
CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
CONF_ON_COMMAND_SENT = "on_command_sent"
CONF_ON_ONLINE = "on_online"
CONF_ON_OFFLINE = "on_offline"
CONF_RAW_ENCODE = "raw_encode"
CONF_REGISTER_COUNT = "register_count"
CONF_REGISTER_TYPE = "register_type"
CONF_RESPONSE_SIZE = "response_size"
CONF_SKIP_UPDATES = "skip_updates"
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
CONF_VALUE_TYPE = "value_type"
CONF_WRITE_LAMBDA = "write_lambda"

View File

@ -0,0 +1,722 @@
#include "modbus_controller.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller";
void ModbusController::setup() { this->create_register_ranges_(); }
/*
To work with the existing modbus class and avoid polling for responses a command queue is used.
send_next_command will submit the command at the top of the queue and set the corresponding callback
to handle the response from the device.
Once the response has been processed it is removed from the queue and the next command is sent
*/
bool ModbusController::send_next_command_() {
uint32_t last_send = millis() - this->last_command_timestamp_;
if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) {
auto &command = this->command_queue_.front();
// remove from queue if command was sent too often
if (!command->should_retry(this->max_cmd_retries_)) {
if (!this->module_offline_) {
ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
if (this->offline_skip_updates_ > 0) {
// Update skip_updates_counter to stop flooding channel with timeouts
for (auto &r : this->register_ranges_) {
r.skip_updates_counter = this->offline_skip_updates_;
}
}
this->module_offline_ = true;
this->offline_callback_.call((int) command->function_code, command->register_address);
}
ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
this->address_, command->register_address);
this->command_queue_.pop_front();
} else {
ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
command->register_address, command->register_count);
command->send();
this->last_command_timestamp_ = millis();
this->command_sent_callback_.call((int) command->function_code, command->register_address);
// remove from queue if no handler is defined
if (!command->on_data_func) {
this->command_queue_.pop_front();
}
}
}
return (!this->command_queue_.empty());
}
// Queue incoming response
void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
auto &current_command = this->command_queue_.front();
if (current_command != nullptr) {
if (this->module_offline_) {
ESP_LOGW(TAG, "Modbus device=%d back online", this->address_);
if (this->offline_skip_updates_ > 0) {
// Restore skip_updates_counter to restore commands updates
for (auto &r : this->register_ranges_) {
r.skip_updates_counter = 0;
}
}
// Restore module online state
this->module_offline_ = false;
this->online_callback_.call((int) current_command->function_code, current_command->register_address);
}
// Move the commandItem to the response queue
current_command->payload = data;
this->incoming_queue_.push(std::move(current_command));
ESP_LOGV(TAG, "Modbus response queued");
this->command_queue_.pop_front();
}
}
// Dispatch the response to the registered handler
void ModbusController::process_modbus_data_(const ModbusCommandItem *response) {
ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address,
response->payload.size());
response->on_data_func(response->register_type, response->register_address, response->payload);
}
void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) {
ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code);
// Remove pending command waiting for a response
auto &current_command = this->command_queue_.front();
if (current_command != nullptr) {
ESP_LOGE(TAG,
"Modbus error - last command: function code=0x%X register address = 0x%X "
"registers count=%d "
"payload size=%zu",
function_code, current_command->register_address, current_command->register_count,
current_command->payload.size());
this->command_queue_.pop_front();
}
}
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
float value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.",
server_register->address, static_cast<uint8_t>(server_register->value_type),
server_register->register_count, value);
std::vector<uint16_t> payload = float_to_payload(value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address);
std::vector<uint8_t> error_response;
error_response.push_back(this->address_);
error_response.push_back(0x81);
error_response.push_back(0x02);
this->send_raw(error_response);
return;
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
auto reg_it = std::find_if(
std::begin(this->register_ranges_), std::end(this->register_ranges_),
[=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); });
if (reg_it == this->register_ranges_.end()) {
ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
} else {
return reg_it->sensors;
}
// not found
return {};
}
void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address);
// loop through all sensors with the same start address
auto sensors = find_sensors_(register_type, start_address);
for (auto *sensor : sensors) {
sensor->parse_and_publish(data);
}
}
void ModbusController::queue_command(const ModbusCommandItem &command) {
if (!this->allow_duplicate_commands_) {
// check if this command is already qeued.
// not very effective but the queue is never really large
for (auto &item : this->command_queue_) {
if (item->is_equal(command)) {
ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
static_cast<uint8_t>(command.register_type), command.register_address, command.register_count);
// update the payload of the queued command
// replaces a previous command
item->payload = command.payload;
return;
}
}
}
this->command_queue_.push_back(make_unique<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) {
// if a custom command is used the user supplied custom_data is only available in the SensorItem.
if (r.register_type == ModbusRegisterType::CUSTOM) {
auto sensors = this->find_sensors_(r.register_type, r.start_address);
if (!sensors.empty()) {
auto sensor = sensors.cbegin();
auto command_item = ModbusCommandItem::create_custom_command(
this, (*sensor)->custom_data,
[this](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data);
});
command_item.register_address = (*sensor)->start_address;
command_item.register_count = (*sensor)->register_count;
command_item.function_code = ModbusFunctionCode::CUSTOM;
queue_command(command_item);
}
} else {
queue_command(ModbusCommandItem::create_read_command(this, r.register_type, r.start_address, r.register_count));
}
r.skip_updates_counter = r.skip_updates; // reset counter to config value
} else {
r.skip_updates_counter--;
}
}
//
// Queue the modbus requests to be send.
// Once we get a response to the command it is removed from the queue and the next command is send
//
void ModbusController::update() {
if (!this->command_queue_.empty()) {
ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size());
} else {
ESP_LOGV(TAG, "Updating modbus component");
}
for (auto &r : this->register_ranges_) {
ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address);
update_range_(r);
}
}
// walk through the sensors and determine the register ranges to read
size_t ModbusController::create_register_ranges_() {
this->register_ranges_.clear();
if (this->parent_->role == modbus::ModbusRole::CLIENT && this->sensorset_.empty()) {
ESP_LOGW(TAG, "No sensors registered");
return 0;
}
// iterator is sorted see SensorItemsComparator for details
auto ix = this->sensorset_.begin();
RegisterRange r = {};
uint8_t buffer_offset = 0;
SensorItem *prev = nullptr;
while (ix != this->sensorset_.end()) {
SensorItem *curr = *ix;
ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
if (r.register_count == 0) {
// this is the first register in range
r.start_address = curr->start_address;
r.register_count = curr->register_count;
r.register_type = curr->register_type;
r.sensors.insert(curr);
r.skip_updates = curr->skip_updates;
r.skip_updates_counter = 0;
buffer_offset = curr->get_register_size();
ESP_LOGV(TAG, "Started new range");
} else {
// this is not the first register in range so it might be possible
// to reuse the last register or extend the current range
if (!curr->force_new_range && r.register_type == curr->register_type &&
curr->register_type != ModbusRegisterType::CUSTOM) {
if (curr->start_address == (r.start_address + r.register_count - prev->register_count) &&
curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) {
// this register can re-use the data from the previous register
// remove this sensore because start_address is changed (sort-order)
ix = this->sensorset_.erase(ix);
curr->start_address = r.start_address;
curr->offset += prev->offset;
this->sensorset_.insert(curr);
// move iterator backwards because it will be incremented later
ix--;
ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address,
curr->register_count, curr->offset);
} else if (curr->start_address == (r.start_address + r.register_count)) {
// this register can extend the current range
// remove this sensore because start_address is changed (sort-order)
ix = this->sensorset_.erase(ix);
curr->start_address = r.start_address;
curr->offset += buffer_offset;
buffer_offset += curr->get_register_size();
r.register_count += curr->register_count;
this->sensorset_.insert(curr);
// move iterator backwards because it will be incremented later
ix--;
ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address,
curr->register_count, curr->offset);
}
}
}
if (curr->start_address == r.start_address && curr->register_type == r.register_type) {
// use the lowest non zero value for the whole range
// Because zero is the default value for skip_updates it is excluded from getting the min value.
if (curr->skip_updates != 0) {
if (r.skip_updates != 0) {
r.skip_updates = std::min(r.skip_updates, curr->skip_updates);
} else {
r.skip_updates = curr->skip_updates;
}
}
// add sensor to this range
r.sensors.insert(curr);
ix++;
} else {
ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
this->register_ranges_.push_back(r);
r = {};
buffer_offset = 0;
// do not increment the iterator here because the current sensor has to be re-evaluated
}
prev = curr;
}
if (r.register_count > 0) {
// Add the last range
ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
this->register_ranges_.push_back(r);
}
return this->register_ranges_.size();
}
void ModbusController::dump_config() {
ESP_LOGCONFIG(TAG, "ModbusController:");
ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
ESP_LOGCONFIG(TAG, " Max Command Retries: %d", this->max_cmd_retries_);
ESP_LOGCONFIG(TAG, " Offline Skip Updates: %d", this->offline_skip_updates_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap");
for (auto &it : this->sensorset_) {
ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
it->get_register_size());
}
ESP_LOGCONFIG(TAG, "ranges");
for (auto &it : this->register_ranges_) {
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
it.start_address, it.register_count, it.skip_updates);
}
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}
void ModbusController::loop() {
// Incoming data to process?
if (!this->incoming_queue_.empty()) {
auto &message = this->incoming_queue_.front();
if (message != nullptr)
this->process_modbus_data_(message.get());
this->incoming_queue_.pop();
} else {
// all messages processed send pending commands
this->send_next_command_();
}
}
void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
const std::vector<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_sensors_() {
ESP_LOGV(TAG, "sensors");
for (auto &it : this->sensorset_) {
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
it->get_register_size(), it->offset);
}
}
ModbusCommandItem ModbusCommandItem::create_read_command(
ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
std::function<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) {
auto decoded_value = decode_value(v);
cmd.payload.push_back(decoded_value[0]);
cmd.payload.push_back(decoded_value[1]);
}
return cmd;
}
ModbusCommandItem ModbusCommandItem::create_write_single_coil(ModbusController *modbusdevice, uint16_t address,
bool value) {
ModbusCommandItem cmd;
cmd.modbusdevice = modbusdevice;
cmd.register_type = ModbusRegisterType::COIL;
cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_COIL;
cmd.register_address = address;
cmd.register_count = 1;
cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
const std::vector<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,
uint16_t value) {
ModbusCommandItem cmd;
cmd.modbusdevice = modbusdevice;
cmd.register_type = ModbusRegisterType::HOLDING;
cmd.function_code = ModbusFunctionCode::WRITE_SINGLE_REGISTER;
cmd.register_address = start_address;
cmd.register_count = 1; // not used here anyways
cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
};
auto decoded_value = decode_value(value);
cmd.payload.push_back(decoded_value[0]);
cmd.payload.push_back(decoded_value[1]);
return cmd;
}
ModbusCommandItem ModbusCommandItem::create_custom_command(
ModbusController *modbusdevice, const std::vector<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 register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Custom Command sent");
};
} else {
cmd.on_data_func = handler;
}
cmd.payload = values;
return cmd;
}
ModbusCommandItem ModbusCommandItem::create_custom_command(
ModbusController *modbusdevice, const std::vector<uint16_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 register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Custom Command sent");
};
} else {
cmd.on_data_func = handler;
}
for (auto v : values) {
cmd.payload.push_back((v >> 8) & 0xFF);
cmd.payload.push_back(v & 0xFF);
}
return cmd;
}
bool ModbusCommandItem::send() {
if (this->function_code != ModbusFunctionCode::CUSTOM) {
modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(),
this->payload.empty() ? nullptr : &this->payload[0]);
} else {
modbusdevice->send_raw(this->payload);
}
this->send_count_++;
ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address,
this->register_count, this->send_count_);
return true;
}
bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) {
// for custom commands we have to check for identical payloads, since
// address/count/type fields will be set to zero
return this->function_code == ModbusFunctionCode::CUSTOM
? this->payload == other.payload
: other.register_address == this->register_address && other.register_count == this->register_count &&
other.register_type == this->register_type && other.function_code == this->function_code;
}
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
case SensorValueType::FP32:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
data.push_back((value & 0xFFFF000000000000) >> 48);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back((value & 0xFFFF00000000) >> 32);
data.push_back((value & 0xFFFF000000000000) >> 48);
break;
default:
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d",
static_cast<uint16_t>(value_type));
break;
}
}
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
size_t size = data.size() - offset;
bool error = false;
switch (sensor_value_type) {
case SensorValueType::U_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
if (size >= 4) {
value = get_data<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);
} else {
error = true;
}
break;
case SensorValueType::S_WORD:
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::S_DWORD:
if (size >= 4) {
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_DWORD_R: {
if (size >= 4) {
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);
} else {
error = true;
}
} break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
// Ignore bitmask for QWORD
if (size >= 8) {
value = get_data<uint64_t>(data, offset);
} else {
error = true;
}
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R: {
// Ignore bitmask for QWORD
if (size >= 8) {
uint64_t tmp = get_data<uint64_t>(data, offset);
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
} else {
error = true;
}
} break;
case SensorValueType::RAW:
default:
break;
}
if (error)
ESP_LOGE(TAG, "not enough data for value");
return value;
}
void ModbusController::add_on_command_sent_callback(std::function<void(int, int)> &&callback) {
this->command_sent_callback_.add(std::move(callback));
}
void ModbusController::add_on_online_callback(std::function<void(int, int)> &&callback) {
this->online_callback_.add(std::move(callback));
}
void ModbusController::add_on_offline_callback(std::function<void(int, int)> &&callback) {
this->offline_callback_.add(std::move(callback));
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,556 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/modbus/modbus.h"
#include "esphome/core/automation.h"
#include <list>
#include <queue>
#include <set>
#include <utility>
#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 : uint8_t {
CUSTOM = 0x0,
COIL = 0x01,
DISCRETE_INPUT = 0x02,
HOLDING = 0x03,
READ = 0x04,
};
enum class SensorValueType : uint8_t {
RAW = 0x00, // variable length
U_WORD = 0x1, // 1 Register unsigned
U_DWORD = 0x2, // 2 Registers unsigned
S_WORD = 0x3, // 1 Register signed
S_DWORD = 0x4, // 2 Registers signed
BIT = 0x5,
U_DWORD_R = 0x6, // 2 Registers unsigned
S_DWORD_R = 0x7, // 2 Registers unsigned
U_QWORD = 0x8,
S_QWORD = 0x9,
U_QWORD_R = 0xA,
S_QWORD_R = 0xB,
FP32 = 0xC,
FP32_R = 0xD
};
inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) {
switch (reg_type) {
case ModbusRegisterType::COIL:
return ModbusFunctionCode::READ_COILS;
break;
case ModbusRegisterType::DISCRETE_INPUT:
return ModbusFunctionCode::READ_DISCRETE_INPUTS;
break;
case ModbusRegisterType::HOLDING:
return ModbusFunctionCode::READ_HOLDING_REGISTERS;
break;
case ModbusRegisterType::READ:
return ModbusFunctionCode::READ_INPUT_REGISTERS;
break;
default:
return ModbusFunctionCode::CUSTOM;
break;
}
}
inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) {
switch (reg_type) {
case ModbusRegisterType::COIL:
return ModbusFunctionCode::WRITE_SINGLE_COIL;
break;
case ModbusRegisterType::DISCRETE_INPUT:
return ModbusFunctionCode::CUSTOM;
break;
case ModbusRegisterType::HOLDING:
return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS;
break;
case ModbusRegisterType::READ:
default:
return ModbusFunctionCode::CUSTOM;
break;
}
}
inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
/** Get a byte from a hex string
* hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34
* hex_byte_from_str("1122",0) returns 0x11
* @param value string containing hex encoding
* @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in
* the hex string is byte_pos * 2
* @return byte value
*/
inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
if (value.length() < pos * 2 + 1)
return 0;
return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
}
/** Get a word from a hex string
* @param value string containing hex encoding
* @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in
* the hex string is byte_pos * 2
* @return word value
*/
inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
}
/** Get a dword from a hex string
* @param value string containing hex encoding
* @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in
* the hex string is byte_pos * 2
* @return dword value
*/
inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
}
/** Get a qword from a hex string
* @param value string containing hex encoding
* @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in
* the hex string is byte_pos * 2
* @return qword value
*/
inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
return static_cast<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 position if the first right set bit in the mask
* Useful for modbus data where more than one value is packed in a 16 bit register
* Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as
* D15 - D8 = hour, D7 - D0 = minute
* To get the hours use mask 0xFF00 and 0x00FF for the minute
* @param data an integral value between 16 aand 32 bits,
* @param bitmask the bitmask to apply
*/
template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
auto result = (mask & data);
if (result == 0 || mask == 0xFFFFFFFF) {
return result;
}
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
if ((mask & (1 << pos)) != 0)
return result >> pos;
}
return 0;
}
/** Convert float value to vector<uint16_t> suitable for sending
* @param data target for payload
* @param value float value to convert
* @param value_type defines if 16/32 or FP32 is used
* @return vector containing the modbus register words in correct order
*/
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
/** Convert vector<uint8_t> response payload to number.
* @param data payload with the data to convert
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
* @param offset offset to the data in data
* @param bitmask bitmask used for masking and shifting
* @return 64-bit number of the payload
*/
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask);
class ModbusController;
class SensorItem {
public:
virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;
void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
size_t virtual get_register_size() const {
if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) {
return 1;
} else { // if CONF_RESPONSE_BYTES is used override the default
return response_bytes > 0 ? response_bytes : register_count * 2;
}
}
// Override register size for modbus devices not using 1 register for one dword
void set_register_size(uint8_t register_size) { response_bytes = register_size; }
ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
SensorValueType sensor_value_type{SensorValueType::RAW};
uint16_t start_address{0};
uint32_t bitmask{0};
uint8_t offset{0};
uint8_t register_count{0};
uint8_t response_bytes{0};
uint16_t skip_updates{0};
std::vector<uint8_t> custom_data{};
bool force_new_range{false};
};
class ServerRegister {
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count,
std::function<float()> read_lambda) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
this->read_lambda = std::move(read_lambda);
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
std::function<float()> read_lambda;
};
// ModbusController::create_register_ranges_ tries to optimize register range
// for this the sensors must be ordered by register_type, start_address and bitmask
class SensorItemsComparator {
public:
bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
// first sort according to register type
if (lhs->register_type != rhs->register_type) {
return lhs->register_type < rhs->register_type;
}
// ensure that sensor with force_new_range set are before the others
if (lhs->force_new_range != rhs->force_new_range) {
return lhs->force_new_range > rhs->force_new_range;
}
// sort by start address
if (lhs->start_address != rhs->start_address) {
return lhs->start_address < rhs->start_address;
}
// sort by offset (ensures update of sensors in ascending order)
if (lhs->offset != rhs->offset) {
return lhs->offset < rhs->offset;
}
// The pointer to the sensor is used last to ensure that
// multiple sensors with the same values can be added with a stable sort order.
return lhs < rhs;
}
};
using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
struct RegisterRange {
uint16_t start_address;
ModbusRegisterType register_type;
uint8_t register_count;
uint16_t skip_updates; // the config value
SensorSet sensors; // all sensors of this range
uint16_t skip_updates_counter; // the running value
};
class ModbusCommandItem {
public:
static const size_t MAX_PAYLOAD_BYTES = 240;
ModbusController *modbusdevice{nullptr};
uint16_t register_address{0};
uint16_t register_count{0};
ModbusFunctionCode function_code{ModbusFunctionCode::CUSTOM};
ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
on_data_func;
std::vector<uint8_t> payload = {};
bool send();
/// Check if the command should be retried based on the max_retries parameter
bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
/// factory methods
/** Create modbus read command
* Function code 02-04
* @param modbusdevice pointer to the device to execute the command
* @param function_code modbus function code for the read command
* @param start_address modbus address of the first register to read
* @param register_count number of registers to read
* @param handler function called when the response is received
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_read_command(
ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
std::function<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 value uint16_t single register value to write
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
uint16_t value);
/** Create modbus write single registers command
* Function 05 (05hex) Write Single Coil
* @param modbusdevice pointer to the device to execute the command
* @param start_address modbus address of the first register to read
* @param value uint16_t data to be written to the registers
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value);
/** Create modbus write multiple registers command
* Function 15 (0Fhex) Write Multiple Coils
* @param modbusdevice pointer to the device to execute the command
* @param start_address modbus address of the first register to read
* @param value bool vector of values to be written to the registers
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address,
const std::vector<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 complete payload must be provided with the
* exception of the crc codes
* @param handler function called when the response is received. Default is just logging a response
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_custom_command(
ModbusController *modbusdevice, const std::vector<uint8_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
&&handler = nullptr);
/** Create custom modbus command
* @param modbusdevice pointer to the device to execute the command
* @param values word vector of data to be sent to the device. The complete payload must be provided with the
* exception of the crc codes
* @param handler function called when the response is received. Default is just logging a response
* @return ModbusCommandItem with the prepared command
*/
static ModbusCommandItem create_custom_command(
ModbusController *modbusdevice, const std::vector<uint16_t> &values,
std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
&&handler = nullptr);
bool is_equal(const ModbusCommandItem &other);
protected:
// wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
/// How many times this command has been sent
uint8_t send_count_{0};
};
/** Modbus controller class.
* Each instance handles the modbus commuinication for all sensors with the same modbus address
*
* all sensor items (sensors, switches, binarysensor ...) are parsed in modbus address ranges.
* when esphome calls ModbusController::Update the commands for each range are created and sent
* Responses for the commands are dispatched to the modbus sensor items.
*/
class ModbusController : public PollingComponent, public modbus::ModbusDevice {
public:
void dump_config() override;
void loop() override;
void setup() override;
void update() override;
/// queues a modbus command in the send queue
void queue_command(const ModbusCommandItem &command);
/// Registers a sensor with the controller. Called by esphomes code generator
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus response was parsed without errors
void on_modbus_data(const std::vector<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;
/// called when a modbus request (function code 3 or 4) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<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);
/// Allow a duplicate command to be sent
void set_allow_duplicate_commands(bool allow_duplicate_commands) {
this->allow_duplicate_commands_ = allow_duplicate_commands;
}
/// get if a duplicate command can be sent
bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; }
/// called by esphome generated code to set the command_throttle period
void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
/// called by esphome generated code to set the offline_skip_updates
void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
/// get the number of queued modbus commands (should be mostly empty)
size_t get_command_queue_length() { return command_queue_.size(); }
/// get if the module is offline, didn't respond the last command
bool get_module_offline() { return module_offline_; }
/// Set callback for commands
void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
/// Set callback for online changes
void add_on_online_callback(std::function<void(int, int)> &&callback);
/// Set callback for offline changes
void add_on_offline_callback(std::function<void(int, int)> &&callback);
/// called by esphome generated code to set the max_cmd_retries.
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
/// get how many times a command will be (re)sent if no response is received
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
protected:
/// parse sensormap_ and create range of sequential addresses
size_t create_register_ranges_();
// find register in sensormap. Returns iterator with all registers having the same start address
SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
/// submit the read command for the address range to the send queue
void update_range_(RegisterRange &r);
/// parse incoming modbus data
void process_modbus_data_(const ModbusCommandItem *response);
/// send the next modbus command from the send queue
bool send_next_command_();
/// dump the parsed sensormap for diagnostics
void dump_sensors_();
/// Collection of all sensors for this component
SensorSet sensorset_;
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Continuous 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_;
/// if duplicate commands can be sent
bool allow_duplicate_commands_{false};
/// when was the last send operation
uint32_t last_command_timestamp_{0};
/// min time in ms between sending modbus commands
uint16_t command_throttle_{0};
/// if module didn't respond the last command
bool module_offline_{false};
/// how many updates to skip if module is offline
uint16_t offline_skip_updates_{0};
/// How many times we will retry a command if we get no response
uint8_t max_cmd_retries_{4};
/// Command sent callback
CallbackManager<void(int, int)> command_sent_callback_{};
/// Server online callback
CallbackManager<void(int, int)> online_callback_{};
/// Server offline callback
CallbackManager<void(int, int)> offline_callback_{};
};
/** Convert vector<uint8_t> response payload to float.
* @param data payload with data
* @param item SensorItem object
* @return float value of data
*/
inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
float float_value;
if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) {
float_value = bit_cast<float>(static_cast<uint32_t>(number));
} else {
float_value = static_cast<float>(number);
}
return float_value;
}
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
int64_t val;
if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);
}
std::vector<uint16_t> data;
number_to_payload(data, val, value_type);
return data;
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,125 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_ID,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_MULTIPLY,
CONF_STEP,
)
from .. import (
MODBUS_WRITE_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_BITMASK,
CONF_CUSTOM_COMMAND,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
CONF_SKIP_UPDATES,
CONF_USE_WRITE_MULTIPLE,
CONF_VALUE_TYPE,
CONF_WRITE_LAMBDA,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusNumber = modbus_controller_ns.class_(
"ModbusNumber", cg.Component, number.Number, SensorItem
)
def validate_min_max(config):
if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]:
raise cv.Invalid("max_value must be greater than min_value")
if config[CONF_MIN_VALUE] < -16777215:
raise cv.Invalid("max_value must be greater than -16777215")
if config[CONF_MAX_VALUE] > 16777215:
raise cv.Invalid("max_value must not be greater than 16777215")
return config
def validate_modbus_number(config):
if CONF_CUSTOM_COMMAND not in config and CONF_ADDRESS not in config:
raise cv.Invalid(
f" {CONF_ADDRESS} is a required property if '{CONF_CUSTOM_COMMAND}:' isn't used"
)
return config
CONFIG_SCHEMA = cv.All(
number.number_schema(ModbusNumber)
.extend(ModbusItemBaseSchema)
.extend(
{
cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum(
MODBUS_WRITE_REGISTER_TYPE
),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
# 24 bits are the maximum value for fp32 before precision is lost
# 0x00FFFFFF = 16777215
cv.Optional(CONF_MAX_VALUE, default=16777215.0): cv.float_,
cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_,
cv.Optional(CONF_STEP, default=1): cv.positive_float,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
validate_min_max,
validate_modbus_number,
)
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
config[CONF_ADDRESS],
byte_offset,
config[CONF_BITMASK],
config[CONF_VALUE_TYPE],
reg_count,
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
)
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(var.set_parent(parent))
cg.add(parent.add_sensor_item(var))
await add_modbus_base_properties(var, config, ModbusNumber)
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusNumber.operator("ptr"), "item"),
(cg.float_, "x"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(float),
)
cg.add(var.set_write_template(template_))

View File

@ -0,0 +1,86 @@
#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) {
float result = payload_to_float(data, *this) / this->multiply_by_;
// Is there a lambda registered
// call it with the pre converted value and the raw data array
if (this->transform_func_.has_value()) {
// the lambda can parse the response itself
auto val = (*this->transform_func_)(this, result, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
result = val.value();
}
}
ESP_LOGD(TAG, "Number new state : %.02f", result);
// this->sensor_->raw_state = result;
this->publish_state(result);
}
void ModbusNumber::control(float value) {
ModbusCommandItem write_cmd;
std::vector<uint16_t> data;
float write_value = value;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, value, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
write_value = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
} else {
write_value = this->multiply_by_ * write_value;
}
if (!data.empty()) {
ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str());
write_cmd = ModbusCommandItem::create_custom_command(
this->parent_, data,
[this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data);
});
} else {
data = float_to_payload(write_value, this->sensor_value_type);
ESP_LOGD(TAG,
"Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)",
this->get_name().c_str(), this->start_address, this->register_count, value, write_value);
// Create and send the write command
if (this->register_count == 1 && !this->use_write_multiple_) {
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2,
data[0]);
} else {
write_cmd = ModbusCommandItem::create_write_multiple_command(
this->parent_, this->start_address + this->offset / 2, this->register_count, data);
}
// publish new value
write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
const std::vector<uint8_t> &data) {
// gets called when the write command is ack'd from the device
this->parent_->on_write_register_response(write_cmd.register_type, start_address, data);
this->publish_state(value);
};
}
this->parent_->queue_command(write_cmd);
this->publish_state(value);
}
void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); }
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/number/number.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/core/component.h"
#include <vector>
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(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) {
this->register_type = register_type;
this->start_address = start_address;
this->offset = offset;
this->bitmask = bitmask;
this->sensor_value_type = value_type;
this->register_count = register_count;
this->skip_updates = skip_updates;
this->force_new_range = force_new_range;
};
void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; }
using transform_func_t = std::function<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; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
void control(float value) override;
optional<transform_func_t> transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
float multiply_by_{1.0};
bool use_write_multiple_{false};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,105 @@
import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY
from .. import (
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
modbus_calc_properties,
modbus_controller_ns,
)
from ..const import (
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
CONF_USE_WRITE_MULTIPLE,
CONF_VALUE_TYPE,
CONF_WRITE_LAMBDA,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusFloatOutput = modbus_controller_ns.class_(
"ModbusFloatOutput", cg.Component, output.FloatOutput, SensorItem
)
ModbusBinaryOutput = modbus_controller_ns.class_(
"ModbusBinaryOutput", cg.Component, output.BinaryOutput, SensorItem
)
CONFIG_SCHEMA = cv.typed_schema(
{
"coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusBinaryOutput),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
"holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend(
{
cv.GenerateID(): cv.declare_id(ModbusFloatOutput),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
SENSOR_VALUE_TYPE
),
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_,
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
}
),
},
lower=True,
key=CONF_REGISTER_TYPE,
default_type="holding",
)
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
# Binary Output
write_template = None
if config[CONF_REGISTER_TYPE] == "coil":
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_ADDRESS],
byte_offset,
)
if CONF_WRITE_LAMBDA in config:
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusBinaryOutput.operator("ptr"), "item"),
(cg.bool_, "x"),
(cg.std_vector.template(cg.uint8).operator("ref"), "payload"),
],
return_type=cg.optional.template(bool),
)
# Float Output
else:
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_ADDRESS],
byte_offset,
config[CONF_VALUE_TYPE],
reg_count,
)
cg.add(var.set_write_multiply(config[CONF_MULTIPLY]))
if CONF_WRITE_LAMBDA in config:
write_template = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusFloatOutput.operator("ptr"), "item"),
(cg.float_, "x"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(float),
)
await output.register_output(var, config)
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
cg.add(var.set_parent(parent))
if write_template:
cg.add(var.set_write_template(write_template))

View File

@ -0,0 +1,111 @@
#include "modbus_output.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.output";
/** Write a value to the device
*
*/
void ModbusFloatOutput::write_state(float value) {
std::vector<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 = this->multiply_by_ * value;
}
// lambda didn't set payload
if (data.empty()) {
data = float_to_payload(value, this->sensor_value_type);
}
ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)",
this->start_address, this->register_count, value, original_value);
// Create and send the write command
ModbusCommandItem write_cmd;
if (this->register_count == 1 && !this->use_write_multiple_) {
write_cmd =
ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset, data[0]);
} else {
write_cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset,
this->register_count, data);
}
this->parent_->queue_command(write_cmd);
}
void ModbusFloatOutput::dump_config() {
ESP_LOGCONFIG(TAG, "Modbus Float Output:");
LOG_FLOAT_OUTPUT(this);
ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address);
ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count);
ESP_LOGCONFIG(TAG, " Value type: %d", static_cast<int>(this->sensor_value_type));
}
// ModbusBinaryOutput
void ModbusBinaryOutput::write_state(bool state) {
// This will be called every time the user requests a state change.
ModbusCommandItem cmd;
std::vector<uint8_t> data;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, state, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
state = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
}
if (!data.empty()) {
ESP_LOGV(TAG, "Modbus binary output write raw: %s", format_hex_pretty(data).c_str());
cmd = ModbusCommandItem::create_custom_command(
this->parent_, data,
[this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
this->parent_->on_write_register_response(cmd.register_type, this->start_address, data);
});
} else {
ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state),
(int) this->register_type, this->start_address, this->offset);
// offset for coil and discrete inputs is the coil/register number not bytes
if (this->use_write_multiple_) {
std::vector<bool> states{state};
cmd = ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states);
} else {
cmd = ModbusCommandItem::create_write_single_coil(this->parent_, this->start_address + this->offset, state);
}
}
this->parent_->queue_command(cmd);
}
void ModbusBinaryOutput::dump_config() {
ESP_LOGCONFIG(TAG, "Modbus Binary Output:");
LOG_BINARY_OUTPUT(this);
ESP_LOGCONFIG(TAG, " Device start address: 0x%X", this->start_address);
ESP_LOGCONFIG(TAG, " Register count: %d", this->register_count);
ESP_LOGCONFIG(TAG, " Value type: %d", static_cast<int>(this->sensor_value_type));
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,76 @@
#pragma once
#include "esphome/components/output/float_output.h"
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace modbus_controller {
class ModbusFloatOutput : public output::FloatOutput, public Component, public SensorItem {
public:
ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) {
this->register_type = ModbusRegisterType::HOLDING;
this->start_address = start_address;
this->offset = offset;
this->bitmask = bitmask;
this->register_count = register_count;
this->sensor_value_type = value_type;
this->skip_updates = 0;
this->start_address += offset;
this->offset = 0;
}
void dump_config() override;
void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; }
// Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<float>(ModbusFloatOutput *, float, std::vector<uint16_t> &)>;
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
void write_state(float value) override;
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
float multiply_by_{1.0};
bool use_write_multiple_{false};
};
class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem {
public:
ModbusBinaryOutput(uint16_t start_address, uint8_t offset) {
this->register_type = ModbusRegisterType::COIL;
this->start_address = start_address;
this->bitmask = bitmask;
this->sensor_value_type = SensorValueType::BIT;
this->skip_updates = 0;
this->register_count = 1;
this->start_address += offset;
this->offset = 0;
}
void dump_config() override;
void set_parent(ModbusController *parent) { this->parent_ = parent; }
// Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<bool>(ModbusBinaryOutput *, bool, std::vector<uint8_t> &)>;
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
void write_state(bool state) override;
optional<write_transform_func_t> write_transform_func_{nullopt};
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,143 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC
from .. import (
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
ModbusController,
SensorItem,
modbus_controller_ns,
)
from ..const import (
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_COUNT,
CONF_SKIP_UPDATES,
CONF_USE_WRITE_MULTIPLE,
CONF_VALUE_TYPE,
CONF_WRITE_LAMBDA,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras", "@stegm"]
CONF_OPTIONSMAP = "optionsmap"
ModbusSelect = modbus_controller_ns.class_(
"ModbusSelect", cg.Component, select.Select, SensorItem
)
def ensure_option_map():
def validator(value):
cv.check_not_templatable(value)
option = cv.All(cv.string_strict)
mapping = cv.All(cv.int_range(-(2**63), 2**63 - 1))
options_map_schema = cv.Schema({option: mapping})
value = options_map_schema(value)
all_values = list(value.values())
unique_values = set(value.values())
if len(all_values) != len(unique_values):
raise cv.Invalid("Mapping values must be unique.")
return value
return validator
def register_count_value_type_min(value):
reg_count = value.get(CONF_REGISTER_COUNT)
if reg_count is not None:
value_type = value[CONF_VALUE_TYPE]
min_register_count = TYPE_REGISTER_MAP[value_type]
if min_register_count > reg_count:
raise cv.Invalid(
f"Value type {value_type} needs at least {min_register_count} registers"
)
return value
INTEGER_SENSOR_VALUE_TYPE = {
key: value for key, value in SENSOR_VALUE_TYPE.items() if not key.startswith("FP")
}
CONFIG_SCHEMA = cv.All(
select.select_schema(ModbusSelect)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{
cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(
INTEGER_SENSOR_VALUE_TYPE
),
cv.Optional(CONF_REGISTER_COUNT): cv.positive_int,
cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean,
cv.Required(CONF_OPTIONSMAP): ensure_option_map(),
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
},
),
register_count_value_type_min,
)
async def to_code(config):
value_type = config[CONF_VALUE_TYPE]
reg_count = config.get(CONF_REGISTER_COUNT)
if reg_count is None:
reg_count = TYPE_REGISTER_MAP[value_type]
options_map = config[CONF_OPTIONSMAP]
var = cg.new_Pvariable(
config[CONF_ID],
value_type,
config[CONF_ADDRESS],
reg_count,
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
list(options_map.values()),
)
await cg.register_component(var, config)
await select.register_select(var, config, options=list(options_map.keys()))
parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(parent.add_sensor_item(var))
cg.add(var.set_parent(parent))
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
if CONF_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_LAMBDA],
[
(ModbusSelect.operator("const_ptr"), "item"),
(cg.int64, "x"),
(
cg.std_vector.template(cg.uint8).operator("const").operator("ref"),
"data",
),
],
return_type=cg.optional.template(cg.std_string),
)
cg.add(var.set_template(template_))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusSelect.operator("const_ptr"), "item"),
(cg.std_string.operator("const").operator("ref"), "x"),
(cg.int64, "value"),
(cg.std_vector.template(cg.uint16).operator("ref"), "payload"),
],
return_type=cg.optional.template(cg.int64),
)
cg.add(var.set_write_template(template_))

View File

@ -0,0 +1,90 @@
#include "modbus_select.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.select";
void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); }
void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask);
ESP_LOGD(TAG, "New select value %lld from payload", value);
optional<std::string> new_state;
if (this->transform_func_.has_value()) {
auto val = (*this->transform_func_)(this, value, data);
if (val.has_value()) {
new_state = *val;
ESP_LOGV(TAG, "lambda returned option %s", new_state->c_str());
}
}
if (!new_state.has_value()) {
auto map_it = std::find(this->mapping_.cbegin(), this->mapping_.cend(), value);
if (map_it != this->mapping_.cend()) {
size_t idx = std::distance(this->mapping_.cbegin(), map_it);
new_state = this->traits.get_options()[idx];
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
} else {
ESP_LOGE(TAG, "No option found for mapping %lld", value);
}
}
if (new_state.has_value()) {
this->publish_state(new_state.value());
}
}
void ModbusSelect::control(const std::string &value) {
auto options = this->traits.get_options();
auto opt_it = std::find(options.cbegin(), options.cend(), value);
size_t idx = std::distance(options.cbegin(), opt_it);
optional<int64_t> mapval = this->mapping_[idx];
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
std::vector<uint16_t> data;
if (this->write_transform_func_.has_value()) {
auto val = (*this->write_transform_func_)(this, value, *mapval, data);
if (val.has_value()) {
mapval = *val;
ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval);
} else {
ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control");
return;
}
}
if (data.empty()) {
number_to_payload(data, *mapval, this->sensor_value_type);
} else {
ESP_LOGV(TAG, "Using payload from write lambda");
}
if (data.empty()) {
ESP_LOGW(TAG, "No payload was created for updating select");
return;
}
const uint16_t write_address = this->start_address + this->offset / 2;
ModbusCommandItem write_cmd;
if ((this->register_count == 1) && (!this->use_write_multiple_)) {
write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, write_address, data[0]);
} else {
write_cmd =
ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, this->register_count, data);
}
this->parent_->queue_command(write_cmd);
if (this->optimistic_)
this->publish_state(value);
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,54 @@
#pragma once
#include <utility>
#include <vector>
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/components/select/select.h"
#include "esphome/core/component.h"
namespace esphome {
namespace modbus_controller {
class ModbusSelect : public Component, public select::Select, public SensorItem {
public:
ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint16_t skip_updates,
bool force_new_range, std::vector<int64_t> mapping) {
this->register_type = ModbusRegisterType::HOLDING; // not configurable
this->sensor_value_type = sensor_value_type;
this->start_address = start_address;
this->offset = 0; // not configurable
this->bitmask = 0xFFFFFFFF; // not configurable
this->register_count = register_count;
this->response_bytes = 0; // not configurable
this->skip_updates = skip_updates;
this->force_new_range = force_new_range;
this->mapping_ = std::move(mapping);
}
using transform_func_t =
std::function<optional<std::string>(ModbusSelect *const, int64_t, const std::vector<uint8_t> &)>;
using write_transform_func_t =
std::function<optional<int64_t>(ModbusSelect *const, const std::string &, int64_t, std::vector<uint16_t> &)>;
void set_parent(ModbusController *const parent) { this->parent_ = parent; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override;
void control(const std::string &value) override;
protected:
std::vector<int64_t> mapping_{};
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
bool optimistic_{false};
optional<transform_func_t> transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,68 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
MODBUS_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_SKIP_UPDATES,
CONF_VALUE_TYPE,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusSensor = modbus_controller_ns.class_(
"ModbusSensor", cg.Component, sensor.Sensor, SensorItem
)
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(ModbusSensor)
.extend(cv.COMPONENT_SCHEMA)
.extend(ModbusItemBaseSchema)
.extend(
{
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int,
}
),
validate_modbus_register,
)
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
value_type = config[CONF_VALUE_TYPE]
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
config[CONF_ADDRESS],
byte_offset,
config[CONF_BITMASK],
value_type,
reg_count,
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
)
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(paren.add_sensor_item(var))
await add_modbus_base_properties(var, config, ModbusSensor)

View File

@ -0,0 +1,31 @@
#include "modbus_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.sensor";
void ModbusSensor::dump_config() { LOG_SENSOR(TAG, "Modbus Controller Sensor", this); }
void ModbusSensor::parse_and_publish(const std::vector<uint8_t> &data) {
float result = payload_to_float(data, *this);
// Is there a lambda registered
// call it with the pre converted value and the raw data array
if (this->transform_func_.has_value()) {
// the lambda can parse the response itself
auto val = (*this->transform_func_)(this, result, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
result = val.value();
}
}
ESP_LOGD(TAG, "Sensor new state: %.02f", result);
// this->sensor_->raw_state = result;
this->publish_state(result);
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,37 @@
#pragma once
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace modbus_controller {
class ModbusSensor : public Component, public sensor::Sensor, public SensorItem {
public:
ModbusSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
SensorValueType value_type, int register_count, uint16_t skip_updates, bool force_new_range) {
this->register_type = register_type;
this->start_address = start_address;
this->offset = offset;
this->bitmask = bitmask;
this->sensor_value_type = value_type;
this->register_count = register_count;
this->skip_updates = skip_updates;
this->force_new_range = force_new_range;
}
void parse_and_publish(const std::vector<uint8_t> &data) override;
void dump_config() override;
using transform_func_t = std::function<optional<float>(ModbusSensor *, float, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,77 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_BITMASK,
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_REGISTER_TYPE,
CONF_SKIP_UPDATES,
CONF_USE_WRITE_MULTIPLE,
CONF_WRITE_LAMBDA,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusSwitch = modbus_controller_ns.class_(
"ModbusSwitch", cg.Component, switch.Switch, SensorItem
)
CONFIG_SCHEMA = cv.All(
switch.switch_schema(ModbusSwitch, default_restore_mode="DISABLED")
.extend(cv.COMPONENT_SCHEMA)
.extend(ModbusItemBaseSchema)
.extend(
{
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
}
),
validate_modbus_register,
)
async def to_code(config):
byte_offset, _ = modbus_calc_properties(config)
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
config[CONF_ADDRESS],
byte_offset,
config[CONF_BITMASK],
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
)
await cg.register_component(var, config)
await switch.register_switch(var, config)
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(var.set_parent(paren))
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
cg.add(paren.add_sensor_item(var))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],
[
(ModbusSwitch.operator("ptr"), "item"),
(cg.bool_, "x"),
(cg.std_vector.template(cg.uint8).operator("ref"), "payload"),
],
return_type=cg.optional.template(bool),
)
cg.add(var.set_write_template(template_))
await add_modbus_base_properties(var, config, ModbusSwitch, bool, bool)

View File

@ -0,0 +1,104 @@
#include "modbus_switch.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.switch";
void ModbusSwitch::setup() {
optional<bool> initial_state = Switch::get_initial_state_with_restore_mode();
if (initial_state.has_value()) {
// if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
if (initial_state.value()) {
this->turn_on();
} else {
this->turn_off();
}
}
}
void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); }
void ModbusSwitch::parse_and_publish(const std::vector<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;
std::vector<uint8_t> data;
// Is there are lambda configured?
if (this->write_transform_func_.has_value()) {
// data is passed by reference
// the lambda can fill the empty vector directly
// in that case the return value is ignored
auto val = (*this->write_transform_func_)(this, state, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
state = val.value();
} else {
ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
return;
}
}
if (!data.empty()) {
ESP_LOGV(TAG, "Modbus Switch write raw: %s", format_hex_pretty(data).c_str());
cmd = ModbusCommandItem::create_custom_command(
this->parent_, data,
[this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
this->parent_->on_write_register_response(cmd.register_type, this->start_address, data);
});
} else {
ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(),
ONOFF(state), (int) this->register_type, this->start_address, this->offset);
if (this->register_type == ModbusRegisterType::COIL) {
// offset for coil and discrete inputs is the coil/register number not bytes
if (this->use_write_multiple_) {
std::vector<bool> states{state};
cmd = ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states);
} else {
cmd = ModbusCommandItem::create_write_single_coil(this->parent_, this->start_address + this->offset, state);
}
} else {
// since offset is in bytes and a register is 16 bits we get the start by adding offset/2
if (this->use_write_multiple_) {
std::vector<uint16_t> bool_states(1, state ? (0xFFFF & this->bitmask) : 0);
cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset / 2, 1,
bool_states);
} else {
cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2,
state ? 0xFFFF & this->bitmask : 0u);
}
}
}
this->parent_->queue_command(cmd);
this->publish_state(state);
}
// ModbusSwitch end
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace modbus_controller {
class ModbusSwitch : public Component, public switch_::Switch, public SensorItem {
public:
ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask,
uint16_t skip_updates, bool force_new_range) {
this->register_type = register_type;
this->start_address = start_address;
this->offset = offset;
this->bitmask = bitmask;
this->sensor_value_type = SensorValueType::BIT;
this->skip_updates = skip_updates;
this->register_count = 1;
if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) {
this->start_address += offset;
this->offset = 0;
}
this->force_new_range = force_new_range;
};
void setup() override;
void write_state(bool state) override;
void dump_config() override;
void set_state(bool state) { this->state = state; }
void parse_and_publish(const std::vector<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> &)>;
using write_transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
optional<transform_func_t> publish_transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
};
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,84 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .. import (
MODBUS_REGISTER_TYPE,
ModbusItemBaseSchema,
SensorItem,
add_modbus_base_properties,
modbus_calc_properties,
modbus_controller_ns,
validate_modbus_register,
)
from ..const import (
CONF_FORCE_NEW_RANGE,
CONF_MODBUS_CONTROLLER_ID,
CONF_RAW_ENCODE,
CONF_REGISTER_COUNT,
CONF_REGISTER_TYPE,
CONF_RESPONSE_SIZE,
CONF_SKIP_UPDATES,
)
DEPENDENCIES = ["modbus_controller"]
CODEOWNERS = ["@martgras"]
ModbusTextSensor = modbus_controller_ns.class_(
"ModbusTextSensor", cg.Component, text_sensor.TextSensor, SensorItem
)
RawEncoding_ns = modbus_controller_ns.namespace("RawEncoding")
RawEncoding = RawEncoding_ns.enum("RawEncoding")
RAW_ENCODING = {
"NONE": RawEncoding.NONE,
"HEXBYTES": RawEncoding.HEXBYTES,
"COMMA": RawEncoding.COMMA,
"ANSI": RawEncoding.ANSI,
}
CONFIG_SCHEMA = cv.All(
text_sensor.text_sensor_schema()
.extend(cv.COMPONENT_SCHEMA)
.extend(ModbusItemBaseSchema)
.extend(
{
cv.GenerateID(): cv.declare_id(ModbusTextSensor),
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int,
cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int,
cv.Optional(CONF_RAW_ENCODE, default="ANSI"): cv.enum(RAW_ENCODING),
}
),
validate_modbus_register,
)
async def to_code(config):
byte_offset, reg_count = modbus_calc_properties(config)
response_size = config[CONF_RESPONSE_SIZE]
reg_count = config[CONF_REGISTER_COUNT]
if reg_count == 0:
reg_count = response_size / 2
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_REGISTER_TYPE],
config[CONF_ADDRESS],
byte_offset,
reg_count,
config[CONF_RESPONSE_SIZE],
config[CONF_RAW_ENCODE],
config[CONF_SKIP_UPDATES],
config[CONF_FORCE_NEW_RANGE],
)
await cg.register_component(var, config)
await text_sensor.register_text_sensor(var, config)
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(paren.add_sensor_item(var))
await add_modbus_base_properties(
var, config, ModbusTextSensor, cg.std_string, cg.std_string
)

View File

@ -0,0 +1,52 @@
#include "modbus_textsensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace modbus_controller {
static const char *const TAG = "modbus_controller.text_sensor";
void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); }
void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
std::string output_str{};
uint8_t items_left = this->response_bytes;
uint8_t index = this->offset;
while ((items_left > 0) && index < data.size()) {
uint8_t b = data[index];
switch (this->encode_) {
case RawEncoding::HEXBYTES:
output_str += str_snprintf("%02x", 2, b);
break;
case RawEncoding::COMMA:
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
break;
case RawEncoding::ANSI:
if (b < 0x20)
break;
// FALLTHROUGH
// Anything else no encoding
default:
output_str += (char) b;
break;
}
items_left--;
index++;
}
// Is there a lambda registered
// call it with the pre converted value and the raw data array
if (this->transform_func_.has_value()) {
// the lambda can parse the response itself
auto val = (*this->transform_func_)(this, output_str, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
output_str = val.value();
}
}
this->publish_state(output_str);
}
} // namespace modbus_controller
} // namespace esphome

View File

@ -0,0 +1,44 @@
#pragma once
#include "esphome/components/modbus_controller/modbus_controller.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace modbus_controller {
enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 };
class ModbusTextSensor : public Component, public text_sensor::TextSensor, public SensorItem {
public:
ModbusTextSensor(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count,
uint16_t response_bytes, RawEncoding encode, uint16_t skip_updates, bool force_new_range) {
this->register_type = register_type;
this->start_address = start_address;
this->offset = offset;
this->response_bytes = response_bytes;
this->register_count = register_count;
this->encode_ = encode;
this->skip_updates = skip_updates;
this->bitmask = 0xFFFFFFFF;
this->sensor_value_type = SensorValueType::RAW;
this->force_new_range = force_new_range;
}
void dump_config() override;
void parse_and_publish(const std::vector<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};
RawEncoding encode_;
};
} // namespace modbus_controller
} // namespace esphome