Add Select for modbus (#3032)

Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
stegm 2022-02-08 10:27:22 +01:00 committed by GitHub
parent 94f944dc9c
commit 58fa63ad88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 361 additions and 83 deletions

View file

@ -115,6 +115,7 @@ esphome/components/modbus_controller/* @martgras
esphome/components/modbus_controller/binary_sensor/* @martgras
esphome/components/modbus_controller/number/* @martgras
esphome/components/modbus_controller/output/* @martgras
esphome/components/modbus_controller/select/* @martgras @stegm
esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras

View file

@ -63,6 +63,7 @@ from esphome.cpp_types import ( # noqa
uint32,
uint64,
int32,
int64,
const_char_ptr,
NAN,
esphome_ns,

View file

@ -110,7 +110,8 @@ void ModbusController::queue_command(const ModbusCommandItem &command) {
for (auto &item : command_queue_) {
if (item->register_address == command.register_address && item->register_count == command.register_count &&
item->register_type == command.register_type && item->function_code == command.function_code) {
ESP_LOGW(TAG, "Duplicate modbus command found");
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;
@ -360,8 +361,9 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusControl
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
};
for (auto v : values) {
cmd.payload.push_back((v / 256) & 0xFF);
cmd.payload.push_back(v & 0xFF);
auto decoded_value = decode_value(v);
cmd.payload.push_back(decoded_value[0]);
cmd.payload.push_back(decoded_value[1]);
}
return cmd;
}
@ -416,7 +418,7 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusControlle
}
ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
int16_t value) {
uint16_t value) {
ModbusCommandItem cmd;
cmd.modbusdevice = modbusdevice;
cmd.register_type = ModbusRegisterType::HOLDING;
@ -427,8 +429,10 @@ ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusControlle
const std::vector<uint8_t> &data) {
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
};
cmd.payload.push_back((value / 256) & 0xFF);
cmd.payload.push_back((value % 256) & 0xFF);
auto decoded_value = decode_value(value);
cmd.payload.push_back(decoded_value[0]);
cmd.payload.push_back(decoded_value[1]);
return cmd;
}
@ -463,84 +467,70 @@ bool ModbusCommandItem::send() {
return true;
}
std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
union {
float float_value;
uint32_t raw;
} raw_to_float;
std::vector<uint16_t> data;
int32_t val;
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:
// cast truncates the float do some rounding here
data.push_back(lroundf(value) & 0xFFFF);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD:
val = lroundf(value);
data.push_back((val & 0xFFFF0000) >> 16);
data.push_back(val & 0xFFFF);
case SensorValueType::FP32:
case SensorValueType::FP32_R:
data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R:
val = lroundf(value);
data.push_back(val & 0xFFFF);
data.push_back((val & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16);
break;
case SensorValueType::FP32:
raw_to_float.float_value = value;
data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16);
data.push_back(raw_to_float.raw & 0xFFFF);
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::FP32_R:
raw_to_float.float_value = value;
data.push_back(raw_to_float.raw & 0xFFFF);
data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16);
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 float to payload conversation");
ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d",
static_cast<uint16_t>(value_type));
break;
}
return data;
}
float payload_to_float(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask) {
union {
float float_value;
uint32_t raw;
} raw_to_float;
int64_t 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
float result = NAN;
switch (sensor_value_type) {
case SensorValueType::U_WORD:
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
result = static_cast<float>(value);
break;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::S_WORD:
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
result = static_cast<float>(value);
break;
case SensorValueType::S_DWORD:
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
result = static_cast<float>(value);
break;
case SensorValueType::S_DWORD_R: {
value = get_data<uint32_t>(data, offset);
@ -549,18 +539,14 @@ float payload_to_float(const std::vector<uint8_t> &data, SensorValueType sensor_
uint32_t sign_bit = (value & 0x8000) << 16;
value = mask_and_shift_by_rightbit(
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
result = static_cast<float>(value);
} break;
case SensorValueType::U_QWORD:
// Ignore bitmask for U_QWORD
value = get_data<uint64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::S_QWORD:
// Ignore bitmask for S_QWORD
value = get_data<int64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::U_QWORD_R:
// Ignore bitmask for U_QWORD
@ -568,32 +554,16 @@ float payload_to_float(const std::vector<uint8_t> &data, SensorValueType sensor_
value = static_cast<uint64_t>(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 |
static_cast<uint64_t>(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 |
static_cast<uint64_t>(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16;
result = static_cast<float>(value);
break;
case SensorValueType::S_QWORD_R:
// Ignore bitmask for S_QWORD
value = get_data<int64_t>(data, offset);
result = static_cast<float>(value);
break;
case SensorValueType::FP32:
raw_to_float.raw = get_data<uint32_t>(data, offset);
ESP_LOGD(TAG, "FP32 = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value);
result = raw_to_float.float_value;
break;
case SensorValueType::FP32_R: {
auto tmp = get_data<uint32_t>(data, offset);
raw_to_float.raw = static_cast<uint32_t>(tmp & 0xFFFF) << 16 | (tmp & 0xFFFF0000) >> 16;
ESP_LOGD(TAG, "FP32_R = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value);
result = raw_to_float.float_value;
} break;
case SensorValueType::RAW:
result = NAN;
break;
default:
break;
}
return result;
return value;
}
} // namespace modbus_controller

View file

@ -195,7 +195,7 @@ inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
*/
template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
auto result = (mask & data);
if (result == 0) {
if (result == 0 || mask == 0xFFFFFFFF) {
return result;
}
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
@ -205,22 +205,23 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return 0;
}
/** convert float value to vector<uint16_t> suitable for sending
* @param value float value to cconvert
/** 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
*/
std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type);
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
/** convert vector<uint8_t> response payload to float
* @param value float value to cconvert
/** 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 float version of the input
* @return 64-bit number of the payload
*/
float payload_to_float(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask);
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
uint32_t bitmask);
class ModbusController;
@ -348,11 +349,11 @@ class ModbusCommandItem {
* @param modbusdevice pointer to the device to execute the command
* @param start_address modbus address of the first register to read
* @param register_count number of registers to read
* @param values uint16_t array to be written to the registers
* @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,
int16_t value);
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
@ -446,13 +447,36 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
uint16_t command_throttle_;
};
/** convert vector<uint8_t> response payload to float
* @param value float value to cconvert
/** Convert vector<uint8_t> response payload to float.
* @param data payload with data
* @param item SensorItem object
* @return float version of the input
* @return float value of data
*/
inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
return payload_to_float(data, item.sensor_value_type, item.offset, item.bitmask);
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

View file

@ -0,0 +1,142 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import select
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA
from esphome.jsonschema import jschema_composite
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
)
@jschema_composite
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.extend(cv.COMPONENT_SCHEMA).extend(
{
cv.GenerateID(): cv.declare_id(ModbusSelect),
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_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]))
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,86 @@
#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(parent_, write_address, data[0]);
} else {
write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, write_address, this->register_count, data);
}
parent_->queue_command(write_cmd);
}
} // namespace modbus_controller
} // namespace esphome

View file

@ -0,0 +1,51 @@
#pragma once
#include <utility>
#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, uint8_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_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_;
bool use_write_multiple_{false};
optional<transform_func_t> transform_func_;
optional<write_transform_func_t> write_transform_func_;
};
} // namespace modbus_controller
} // namespace esphome

View file

@ -773,9 +773,11 @@ class MockObj(Expression):
return MockObj(f"{self.base} &", "")
if name == "ptr":
return MockObj(f"{self.base} *", "")
if name == "const_ptr":
return MockObj(f"{self.base} *const", "")
if name == "const":
return MockObj(f"const {self.base}", "")
raise ValueError("Expected one of ref, ptr, const.")
raise ValueError("Expected one of ref, ptr, const_ptr, const.")
@property
def using(self) -> "MockObj":

View file

@ -15,6 +15,7 @@ uint16 = global_ns.namespace("uint16_t")
uint32 = global_ns.namespace("uint32_t")
uint64 = global_ns.namespace("uint64_t")
int32 = global_ns.namespace("int32_t")
int64 = global_ns.namespace("int64_t")
const_char_ptr = global_ns.namespace("const char *")
NAN = global_ns.namespace("NAN")
esphome_ns = global_ns # using namespace esphome;