[modbus] Decode by register address in the modbus sensor components (#18874)

This commit is contained in:
Bonne Eggleston
2026-08-28 18:44:37 -05:00
committed by GitHub
parent 1c44cec343
commit bcec1d6cb8
7 changed files with 88 additions and 102 deletions
@@ -3,6 +3,8 @@
namespace esphome::growatt_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "growatt_solar";
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
@@ -16,23 +18,18 @@ void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<con
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg, float unit) -> void {
if (sensor == nullptr || reg < start_address)
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset >= registers.size())
return;
sensor->publish_state(registers[offset] * unit);
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg, float unit) -> void {
constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD;
if (sensor == nullptr || reg < start_address)
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset) * unit);
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
switch (this->protocol_version_) {
@@ -17,53 +17,53 @@ enum GrowattProtocolVersion {
};
// Register addresses for the RTU protocol.
constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
constexpr uint16_t RTU_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
constexpr uint16_t RTU_GRID_FREQUENCY = 13; // length = 1
constexpr uint16_t RTU_PHASE1_VOLTAGE = 14; // length = 1
constexpr uint16_t RTU_PHASE1_CURRENT = 15; // length = 1
constexpr uint16_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
constexpr uint16_t RTU_PHASE2_VOLTAGE = 18; // length = 1
constexpr uint16_t RTU_PHASE2_CURRENT = 19; // length = 1
constexpr uint16_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
constexpr uint16_t RTU_PHASE3_VOLTAGE = 22; // length = 1
constexpr uint16_t RTU_PHASE3_CURRENT = 23; // length = 1
constexpr uint16_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
constexpr uint16_t RTU_TODAY_PRODUCTION = 26; // length = 2
constexpr uint16_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
constexpr uint16_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
// Input register addresses for the RTU2 protocol as described
// in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document.
constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
constexpr uint16_t RTU2_INVERTER_STATUS = 0; // length = 1
constexpr uint16_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
constexpr uint16_t RTU2_PV1_VOLTAGE = 3; // length = 1
constexpr uint16_t RTU2_PV1_CURRENT = 4; // length = 1
constexpr uint16_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
constexpr uint16_t RTU2_PV2_VOLTAGE = 7; // length = 1
constexpr uint16_t RTU2_PV2_CURRENT = 8; // length = 1
constexpr uint16_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
constexpr uint16_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
constexpr uint16_t RTU2_GRID_FREQUENCY = 37; // length = 1
constexpr uint16_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
constexpr uint16_t RTU2_PHASE1_CURRENT = 39; // length = 1
constexpr uint16_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
constexpr uint16_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
constexpr uint16_t RTU2_PHASE2_CURRENT = 43; // length = 1
constexpr uint16_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
constexpr uint16_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
constexpr uint16_t RTU2_PHASE3_CURRENT = 47; // length = 1
constexpr uint16_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
constexpr uint16_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr uint16_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr uint16_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice {
public:
@@ -4,6 +4,8 @@
namespace esphome::havells_solar {
namespace helpers = modbus::helpers;
static const char *const TAG = "havells_solar";
static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers
@@ -16,22 +18,17 @@ void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span<c
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset >= registers.size())
return;
sensor->publish_state(registers[offset] * unit);
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD;
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset) * unit);
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
for (uint8_t i = 0; i < 3; i++) {
+8 -11
View File
@@ -3,6 +3,8 @@
namespace esphome::pzemac {
namespace helpers = modbus::helpers;
static const char *const TAG = "pzemac";
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
@@ -25,22 +27,17 @@ void PZEMAC::on_read_input_registers(uint16_t start_address, std::span<const uin
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset >= registers.size())
return;
sensor->publish_state(registers[offset] / divisor);
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R;
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset) / divisor);
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 10.0f);
+8 -11
View File
@@ -3,6 +3,8 @@
namespace esphome::pzemdc {
namespace helpers = modbus::helpers;
static const char *const TAG = "pzemdc";
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
@@ -23,22 +25,17 @@ void PZEMDC::on_read_input_registers(uint16_t start_address, std::span<const uin
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
// correct for any read range, so the poll may be split into multiple requests.
auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset >= registers.size())
return;
sensor->publish_state(registers[offset] / divisor);
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R;
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset) / divisor);
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, start_address, reg))
sensor->publish_state(*value / divisor);
};
publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 100.0f);
+5 -6
View File
@@ -4,6 +4,8 @@
namespace esphome::sdm_meter {
namespace helpers = modbus::helpers;
static const char *const TAG = "sdm_meter";
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 80 x 16-bit registers (40 float values)
@@ -16,13 +18,10 @@ void SDMMeter::on_read_input_registers(uint16_t start_address, std::span<const u
// Publish a sensor if both of its registers are in this response; skipping absent registers keeps
// this correct for any read range, so the poll may be split into multiple requests.
auto publish = [&](uint16_t reg, sensor::Sensor *sensor) {
constexpr auto value_type = modbus::helpers::SensorValueType::FP32;
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset));
if (auto value = helpers::value_at<helpers::SensorValueType::FP32>(registers, start_address, reg))
sensor->publish_state(*value);
};
for (uint8_t i = 0; i < 3; i++) {
@@ -4,6 +4,8 @@
namespace esphome::selec_meter {
namespace helpers = modbus::helpers;
static const char *const TAG = "selec_meter";
static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers
@@ -17,13 +19,10 @@ void SelecMeter::on_read_input_registers(uint16_t start_address, std::span<const
// this correct for any read range, so the poll may be split into multiple requests.
// Values are 32-bit floats, low word first.
auto publish = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
constexpr auto value_type = modbus::helpers::SensorValueType::FP32_R;
if (sensor == nullptr || reg < start_address)
if (sensor == nullptr)
return;
size_t offset = reg - start_address;
if (offset + modbus::helpers::register_width_for(value_type) > registers.size())
return;
sensor->publish_state(modbus::helpers::registers_to_value<value_type>(registers.data() + offset) * unit);
if (auto value = helpers::value_at<helpers::SensorValueType::FP32_R>(registers, start_address, reg))
sensor->publish_state(*value * unit);
};
publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT);