mirror of
https://github.com/esphome/esphome.git
synced 2024-11-10 01:07:45 +01:00
Add Select for modbus (#3032)
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
parent
94f944dc9c
commit
58fa63ad88
9 changed files with 361 additions and 83 deletions
|
@ -115,6 +115,7 @@ esphome/components/modbus_controller/* @martgras
|
||||||
esphome/components/modbus_controller/binary_sensor/* @martgras
|
esphome/components/modbus_controller/binary_sensor/* @martgras
|
||||||
esphome/components/modbus_controller/number/* @martgras
|
esphome/components/modbus_controller/number/* @martgras
|
||||||
esphome/components/modbus_controller/output/* @martgras
|
esphome/components/modbus_controller/output/* @martgras
|
||||||
|
esphome/components/modbus_controller/select/* @martgras @stegm
|
||||||
esphome/components/modbus_controller/sensor/* @martgras
|
esphome/components/modbus_controller/sensor/* @martgras
|
||||||
esphome/components/modbus_controller/switch/* @martgras
|
esphome/components/modbus_controller/switch/* @martgras
|
||||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||||
|
|
|
@ -63,6 +63,7 @@ from esphome.cpp_types import ( # noqa
|
||||||
uint32,
|
uint32,
|
||||||
uint64,
|
uint64,
|
||||||
int32,
|
int32,
|
||||||
|
int64,
|
||||||
const_char_ptr,
|
const_char_ptr,
|
||||||
NAN,
|
NAN,
|
||||||
esphome_ns,
|
esphome_ns,
|
||||||
|
|
|
@ -110,7 +110,8 @@ void ModbusController::queue_command(const ModbusCommandItem &command) {
|
||||||
for (auto &item : command_queue_) {
|
for (auto &item : command_queue_) {
|
||||||
if (item->register_address == command.register_address && item->register_count == command.register_count &&
|
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) {
|
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
|
// update the payload of the queued command
|
||||||
// replaces a previous command
|
// replaces a previous command
|
||||||
item->payload = command.payload;
|
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);
|
modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
|
||||||
};
|
};
|
||||||
for (auto v : values) {
|
for (auto v : values) {
|
||||||
cmd.payload.push_back((v / 256) & 0xFF);
|
auto decoded_value = decode_value(v);
|
||||||
cmd.payload.push_back(v & 0xFF);
|
cmd.payload.push_back(decoded_value[0]);
|
||||||
|
cmd.payload.push_back(decoded_value[1]);
|
||||||
}
|
}
|
||||||
return cmd;
|
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,
|
ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
|
||||||
int16_t value) {
|
uint16_t value) {
|
||||||
ModbusCommandItem cmd;
|
ModbusCommandItem cmd;
|
||||||
cmd.modbusdevice = modbusdevice;
|
cmd.modbusdevice = modbusdevice;
|
||||||
cmd.register_type = ModbusRegisterType::HOLDING;
|
cmd.register_type = ModbusRegisterType::HOLDING;
|
||||||
|
@ -427,8 +429,10 @@ ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusControlle
|
||||||
const std::vector<uint8_t> &data) {
|
const std::vector<uint8_t> &data) {
|
||||||
modbusdevice->on_write_register_response(cmd.register_type, start_address, 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;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,84 +467,70 @@ bool ModbusCommandItem::send() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
union {
|
|
||||||
float float_value;
|
|
||||||
uint32_t raw;
|
|
||||||
} raw_to_float;
|
|
||||||
|
|
||||||
std::vector<uint16_t> data;
|
|
||||||
int32_t val;
|
|
||||||
|
|
||||||
switch (value_type) {
|
switch (value_type) {
|
||||||
case SensorValueType::U_WORD:
|
case SensorValueType::U_WORD:
|
||||||
case SensorValueType::S_WORD:
|
case SensorValueType::S_WORD:
|
||||||
// cast truncates the float do some rounding here
|
data.push_back(value & 0xFFFF);
|
||||||
data.push_back(lroundf(value) & 0xFFFF);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_DWORD:
|
case SensorValueType::U_DWORD:
|
||||||
case SensorValueType::S_DWORD:
|
case SensorValueType::S_DWORD:
|
||||||
val = lroundf(value);
|
case SensorValueType::FP32:
|
||||||
data.push_back((val & 0xFFFF0000) >> 16);
|
case SensorValueType::FP32_R:
|
||||||
data.push_back(val & 0xFFFF);
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
|
data.push_back(value & 0xFFFF);
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_DWORD_R:
|
case SensorValueType::U_DWORD_R:
|
||||||
case SensorValueType::S_DWORD_R:
|
case SensorValueType::S_DWORD_R:
|
||||||
val = lroundf(value);
|
data.push_back(value & 0xFFFF);
|
||||||
data.push_back(val & 0xFFFF);
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
data.push_back((val & 0xFFFF0000) >> 16);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::FP32:
|
case SensorValueType::U_QWORD:
|
||||||
raw_to_float.float_value = value;
|
case SensorValueType::S_QWORD:
|
||||||
data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16);
|
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||||
data.push_back(raw_to_float.raw & 0xFFFF);
|
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||||
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
|
data.push_back(value & 0xFFFF);
|
||||||
break;
|
break;
|
||||||
case SensorValueType::FP32_R:
|
case SensorValueType::U_QWORD_R:
|
||||||
raw_to_float.float_value = value;
|
case SensorValueType::S_QWORD_R:
|
||||||
data.push_back(raw_to_float.raw & 0xFFFF);
|
data.push_back(value & 0xFFFF);
|
||||||
data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16);
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
|
data.push_back((value & 0xFFFF00000000) >> 32);
|
||||||
|
data.push_back((value & 0xFFFF000000000000) >> 48);
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float payload_to_float(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||||
uint32_t bitmask) {
|
uint32_t bitmask) {
|
||||||
union {
|
|
||||||
float float_value;
|
|
||||||
uint32_t raw;
|
|
||||||
} raw_to_float;
|
|
||||||
|
|
||||||
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
|
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
|
||||||
float result = NAN;
|
|
||||||
|
|
||||||
switch (sensor_value_type) {
|
switch (sensor_value_type) {
|
||||||
case SensorValueType::U_WORD:
|
case SensorValueType::U_WORD:
|
||||||
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_DWORD:
|
case SensorValueType::U_DWORD:
|
||||||
|
case SensorValueType::FP32:
|
||||||
value = get_data<uint32_t>(data, offset);
|
value = get_data<uint32_t>(data, offset);
|
||||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_DWORD_R:
|
case SensorValueType::U_DWORD_R:
|
||||||
|
case SensorValueType::FP32_R:
|
||||||
value = get_data<uint32_t>(data, offset);
|
value = get_data<uint32_t>(data, offset);
|
||||||
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
||||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::S_WORD:
|
case SensorValueType::S_WORD:
|
||||||
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
|
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
|
||||||
bitmask); // default is 0xFFFF ;
|
bitmask); // default is 0xFFFF ;
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::S_DWORD:
|
case SensorValueType::S_DWORD:
|
||||||
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::S_DWORD_R: {
|
case SensorValueType::S_DWORD_R: {
|
||||||
value = get_data<uint32_t>(data, offset);
|
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;
|
uint32_t sign_bit = (value & 0x8000) << 16;
|
||||||
value = mask_and_shift_by_rightbit(
|
value = mask_and_shift_by_rightbit(
|
||||||
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
||||||
result = static_cast<float>(value);
|
|
||||||
} break;
|
} break;
|
||||||
case SensorValueType::U_QWORD:
|
case SensorValueType::U_QWORD:
|
||||||
// Ignore bitmask for U_QWORD
|
// Ignore bitmask for U_QWORD
|
||||||
value = get_data<uint64_t>(data, offset);
|
value = get_data<uint64_t>(data, offset);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SensorValueType::S_QWORD:
|
case SensorValueType::S_QWORD:
|
||||||
// Ignore bitmask for S_QWORD
|
// Ignore bitmask for S_QWORD
|
||||||
value = get_data<int64_t>(data, offset);
|
value = get_data<int64_t>(data, offset);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_QWORD_R:
|
case SensorValueType::U_QWORD_R:
|
||||||
// Ignore bitmask for U_QWORD
|
// 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 |
|
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 & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 |
|
||||||
static_cast<uint64_t>(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16;
|
static_cast<uint64_t>(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16;
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SensorValueType::S_QWORD_R:
|
case SensorValueType::S_QWORD_R:
|
||||||
// Ignore bitmask for S_QWORD
|
// Ignore bitmask for S_QWORD
|
||||||
value = get_data<int64_t>(data, offset);
|
value = get_data<int64_t>(data, offset);
|
||||||
result = static_cast<float>(value);
|
|
||||||
break;
|
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:
|
case SensorValueType::RAW:
|
||||||
result = NAN;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace modbus_controller
|
} // namespace modbus_controller
|
||||||
|
|
|
@ -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) {
|
template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||||
auto result = (mask & data);
|
auto result = (mask & data);
|
||||||
if (result == 0) {
|
if (result == 0 || mask == 0xFFFFFFFF) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
|
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
|
||||||
|
@ -205,21 +205,22 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** convert float value to vector<uint16_t> suitable for sending
|
/** Convert float value to vector<uint16_t> suitable for sending
|
||||||
* @param value float value to cconvert
|
* @param data target for payload
|
||||||
|
* @param value float value to convert
|
||||||
* @param value_type defines if 16/32 or FP32 is used
|
* @param value_type defines if 16/32 or FP32 is used
|
||||||
* @return vector containing the modbus register words in correct order
|
* @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
|
/** Convert vector<uint8_t> response payload to number.
|
||||||
* @param value float value to cconvert
|
* @param data payload with the data to convert
|
||||||
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
|
* @param sensor_value_type defines if 16/32/64 bits or FP32 is used
|
||||||
* @param offset offset to the data in data
|
* @param offset offset to the data in data
|
||||||
* @param bitmask bitmask used for masking and shifting
|
* @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,
|
int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
|
||||||
uint32_t bitmask);
|
uint32_t bitmask);
|
||||||
|
|
||||||
class ModbusController;
|
class ModbusController;
|
||||||
|
@ -348,11 +349,11 @@ class ModbusCommandItem {
|
||||||
* @param modbusdevice pointer to the device to execute the command
|
* @param modbusdevice pointer to the device to execute the command
|
||||||
* @param start_address modbus address of the first register to read
|
* @param start_address modbus address of the first register to read
|
||||||
* @param register_count number of registers 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
|
* @return ModbusCommandItem with the prepared command
|
||||||
*/
|
*/
|
||||||
static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
|
static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
|
||||||
int16_t value);
|
uint16_t value);
|
||||||
/** Create modbus write single registers command
|
/** Create modbus write single registers command
|
||||||
* Function 05 (05hex) Write Single Coil
|
* Function 05 (05hex) Write Single Coil
|
||||||
* @param modbusdevice pointer to the device to execute the command
|
* @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_;
|
uint16_t command_throttle_;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** convert vector<uint8_t> response payload to float
|
/** Convert vector<uint8_t> response payload to float.
|
||||||
* @param value float value to cconvert
|
* @param data payload with data
|
||||||
* @param item SensorItem object
|
* @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) {
|
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
|
} // namespace modbus_controller
|
||||||
|
|
142
esphome/components/modbus_controller/select/__init__.py
Normal file
142
esphome/components/modbus_controller/select/__init__.py
Normal 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_))
|
|
@ -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
|
51
esphome/components/modbus_controller/select/modbus_select.h
Normal file
51
esphome/components/modbus_controller/select/modbus_select.h
Normal 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
|
|
@ -773,9 +773,11 @@ class MockObj(Expression):
|
||||||
return MockObj(f"{self.base} &", "")
|
return MockObj(f"{self.base} &", "")
|
||||||
if name == "ptr":
|
if name == "ptr":
|
||||||
return MockObj(f"{self.base} *", "")
|
return MockObj(f"{self.base} *", "")
|
||||||
|
if name == "const_ptr":
|
||||||
|
return MockObj(f"{self.base} *const", "")
|
||||||
if name == "const":
|
if name == "const":
|
||||||
return MockObj(f"const {self.base}", "")
|
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
|
@property
|
||||||
def using(self) -> "MockObj":
|
def using(self) -> "MockObj":
|
||||||
|
|
|
@ -15,6 +15,7 @@ uint16 = global_ns.namespace("uint16_t")
|
||||||
uint32 = global_ns.namespace("uint32_t")
|
uint32 = global_ns.namespace("uint32_t")
|
||||||
uint64 = global_ns.namespace("uint64_t")
|
uint64 = global_ns.namespace("uint64_t")
|
||||||
int32 = global_ns.namespace("int32_t")
|
int32 = global_ns.namespace("int32_t")
|
||||||
|
int64 = global_ns.namespace("int64_t")
|
||||||
const_char_ptr = global_ns.namespace("const char *")
|
const_char_ptr = global_ns.namespace("const char *")
|
||||||
NAN = global_ns.namespace("NAN")
|
NAN = global_ns.namespace("NAN")
|
||||||
esphome_ns = global_ns # using namespace esphome;
|
esphome_ns = global_ns # using namespace esphome;
|
||||||
|
|
Loading…
Reference in a new issue