mirror of
https://github.com/esphome/esphome.git
synced 2026-09-24 05:24:14 +00:00
Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
This commit is contained in:
@@ -1,105 +1,68 @@
|
||||
#include "growatt_solar.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
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
|
||||
|
||||
void GrowattSolar::loop() {
|
||||
// If update() was unable to send we retry until we can send.
|
||||
if (!this->waiting_to_update_)
|
||||
return;
|
||||
update();
|
||||
}
|
||||
void GrowattSolar::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); }
|
||||
|
||||
void GrowattSolar::update() {
|
||||
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_send_ < this->get_update_interval() / 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
|
||||
if (!this->ready_for_immediate_send()) {
|
||||
this->waiting_to_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this->waiting_to_update_ = false;
|
||||
this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
|
||||
this->last_send_ = millis();
|
||||
}
|
||||
|
||||
void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
// Other components might be sending commands to our device. But we don't get called with enough
|
||||
// context to know what is what. So if we didn't do a send, we ignore the data.
|
||||
if (!this->last_send_)
|
||||
return;
|
||||
this->last_send_ = 0;
|
||||
|
||||
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
|
||||
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
|
||||
void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return;
|
||||
|
||||
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
|
||||
// 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, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit;
|
||||
sensor->publish_state(value);
|
||||
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 reg1, size_t reg2, float unit) -> void {
|
||||
float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) +
|
||||
encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) *
|
||||
unit;
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(value);
|
||||
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
switch (this->protocol_version_) {
|
||||
case RTU: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER,
|
||||
RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER,
|
||||
RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER,
|
||||
RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
@@ -107,42 +70,33 @@ void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<c
|
||||
case RTU2: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU2_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER,
|
||||
RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER,
|
||||
RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER,
|
||||
RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
|
||||
@@ -17,59 +17,59 @@ 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:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
|
||||
@@ -104,9 +104,6 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
}
|
||||
|
||||
protected:
|
||||
bool waiting_to_update_{false};
|
||||
uint32_t last_send_{0};
|
||||
|
||||
struct GrowattPhase {
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
||||
@@ -1,124 +1,71 @@
|
||||
#include "havells_solar.h"
|
||||
#include "havells_solar_registers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
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
|
||||
|
||||
void HavellsSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for HavellsSolar!");
|
||||
return;
|
||||
}
|
||||
void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
/* Usage: returns the float value of 1 register read by modbus
|
||||
Arg1: Register address * number of bytes per register
|
||||
Arg2: Multiplier for final register value
|
||||
*/
|
||||
auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float {
|
||||
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
|
||||
return temp * unit;
|
||||
// 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)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
/* Usage: returns the float value of 2 registers read by modbus
|
||||
Arg1: Register address * number of bytes per register
|
||||
Arg2: Multiplier for final register value
|
||||
*/
|
||||
auto havells_solar_get_1_register = [&](size_t i, float unit) -> float {
|
||||
uint16_t temp = encode_uint16(data[i], data[i + 1]);
|
||||
return temp * unit;
|
||||
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
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++) {
|
||||
auto phase = this->phases_[i];
|
||||
auto &phase = this->phases_[i];
|
||||
if (!phase.setup)
|
||||
continue;
|
||||
|
||||
float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
|
||||
float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
|
||||
|
||||
if (phase.voltage_sensor_ != nullptr)
|
||||
phase.voltage_sensor_->publish_state(voltage);
|
||||
if (phase.current_sensor_ != nullptr)
|
||||
phase.current_sensor_->publish_state(current);
|
||||
publish_1_register(phase.voltage_sensor_, HAVELLS_PHASE_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
|
||||
publish_1_register(phase.current_sensor_, HAVELLS_PHASE_1_CURRENT + i * 2, TWO_DEC_UNIT);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
auto pv = this->pvs_[i];
|
||||
auto &pv = this->pvs_[i];
|
||||
if (!pv.setup)
|
||||
continue;
|
||||
|
||||
float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
|
||||
float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
|
||||
float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT);
|
||||
float voltage_sampled_by_secondary_cpu =
|
||||
havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT);
|
||||
float insulation_of_p_to_ground =
|
||||
havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT);
|
||||
|
||||
if (pv.voltage_sensor_ != nullptr)
|
||||
pv.voltage_sensor_->publish_state(voltage);
|
||||
if (pv.current_sensor_ != nullptr)
|
||||
pv.current_sensor_->publish_state(current);
|
||||
if (pv.active_power_sensor_ != nullptr)
|
||||
pv.active_power_sensor_->publish_state(active_power);
|
||||
if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr)
|
||||
pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu);
|
||||
if (pv.insulation_of_p_to_ground_sensor_ != nullptr)
|
||||
pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground);
|
||||
publish_1_register(pv.voltage_sensor_, HAVELLS_PV_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
|
||||
publish_1_register(pv.current_sensor_, HAVELLS_PV_1_CURRENT + i * 2, TWO_DEC_UNIT);
|
||||
publish_1_register(pv.active_power_sensor_, HAVELLS_PV_1_POWER + i, MULTIPLY_TEN_UNIT);
|
||||
publish_1_register(pv.voltage_sampled_by_secondary_cpu_sensor_, HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU + i,
|
||||
ONE_DEC_UNIT);
|
||||
publish_1_register(pv.insulation_of_p_to_ground_sensor_, HAVELLS_PV1_INSULATION_OF_P_TO_GROUND + i, NO_DEC_UNIT);
|
||||
}
|
||||
|
||||
float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT);
|
||||
float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT);
|
||||
float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT);
|
||||
float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT);
|
||||
float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT);
|
||||
float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT);
|
||||
float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT);
|
||||
float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT);
|
||||
float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT);
|
||||
float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT);
|
||||
float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT);
|
||||
float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT);
|
||||
float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT);
|
||||
float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT);
|
||||
float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT);
|
||||
|
||||
if (this->frequency_sensor_ != nullptr)
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
if (this->active_power_sensor_ != nullptr)
|
||||
this->active_power_sensor_->publish_state(active_power);
|
||||
if (this->reactive_power_sensor_ != nullptr)
|
||||
this->reactive_power_sensor_->publish_state(reactive_power);
|
||||
if (this->today_production_sensor_ != nullptr)
|
||||
this->today_production_sensor_->publish_state(today_production);
|
||||
if (this->total_energy_production_sensor_ != nullptr)
|
||||
this->total_energy_production_sensor_->publish_state(total_energy_production);
|
||||
if (this->total_generation_time_sensor_ != nullptr)
|
||||
this->total_generation_time_sensor_->publish_state(total_generation_time);
|
||||
if (this->today_generation_time_sensor_ != nullptr)
|
||||
this->today_generation_time_sensor_->publish_state(today_generation_time);
|
||||
if (this->inverter_module_temp_sensor_ != nullptr)
|
||||
this->inverter_module_temp_sensor_->publish_state(inverter_module_temp);
|
||||
if (this->inverter_inner_temp_sensor_ != nullptr)
|
||||
this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp);
|
||||
if (this->inverter_bus_voltage_sensor_ != nullptr)
|
||||
this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage);
|
||||
if (this->insulation_pv_n_to_ground_sensor_ != nullptr)
|
||||
this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground);
|
||||
if (this->gfci_value_sensor_ != nullptr)
|
||||
this->gfci_value_sensor_->publish_state(gfci_value);
|
||||
if (this->dci_of_r_sensor_ != nullptr)
|
||||
this->dci_of_r_sensor_->publish_state(dci_of_r);
|
||||
if (this->dci_of_s_sensor_ != nullptr)
|
||||
this->dci_of_s_sensor_->publish_state(dci_of_s);
|
||||
if (this->dci_of_t_sensor_ != nullptr)
|
||||
this->dci_of_t_sensor_->publish_state(dci_of_t);
|
||||
publish_1_register(this->frequency_sensor_, HAVELLS_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
publish_1_register(this->active_power_sensor_, HAVELLS_SYSTEM_ACTIVE_POWER, MULTIPLY_TEN_UNIT);
|
||||
publish_1_register(this->reactive_power_sensor_, HAVELLS_SYSTEM_REACTIVE_POWER, TWO_DEC_UNIT);
|
||||
publish_1_register(this->today_production_sensor_, HAVELLS_TODAY_PRODUCTION, TWO_DEC_UNIT);
|
||||
publish_2_registers(this->total_energy_production_sensor_, HAVELLS_TOTAL_ENERGY_PRODUCTION, NO_DEC_UNIT);
|
||||
publish_2_registers(this->total_generation_time_sensor_, HAVELLS_TOTAL_GENERATION_TIME, NO_DEC_UNIT);
|
||||
publish_1_register(this->today_generation_time_sensor_, HAVELLS_TODAY_GENERATION_TIME, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_module_temp_sensor_, HAVELLS_INVERTER_MODULE_TEMP, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_inner_temp_sensor_, HAVELLS_INVERTER_INNER_TEMP, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_bus_voltage_sensor_, HAVELLS_INVERTER_BUS_VOLTAGE, NO_DEC_UNIT);
|
||||
publish_1_register(this->insulation_pv_n_to_ground_sensor_, HAVELLS_INSULATION_OF_PV_N_TO_GROUND, NO_DEC_UNIT);
|
||||
publish_1_register(this->gfci_value_sensor_, HAVELLS_GFCI_VALUE, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_r_sensor_, HAVELLS_DCI_OF_R, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_s_sensor_, HAVELLS_DCI_OF_S, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_t_sensor_, HAVELLS_DCI_OF_T, NO_DEC_UNIT);
|
||||
}
|
||||
|
||||
void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); }
|
||||
|
||||
@@ -77,7 +77,8 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -1,87 +1,74 @@
|
||||
#include "kuntze.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::kuntze {
|
||||
|
||||
static const char *const TAG = "kuntze";
|
||||
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
static constexpr uint16_t REGISTER_PH = 4136;
|
||||
static constexpr uint16_t REGISTER_TEMPERATURE = 4160;
|
||||
static constexpr uint16_t REGISTER_DIS1 = 4680;
|
||||
static constexpr uint16_t REGISTER_DIS2 = 6000;
|
||||
static constexpr uint16_t REGISTER_REDOX = 4688;
|
||||
static constexpr uint16_t REGISTER_EC = 4728;
|
||||
static constexpr uint16_t REGISTER_OCI = 5832;
|
||||
static constexpr uint16_t REGISTER[] = {REGISTER_PH, REGISTER_TEMPERATURE, REGISTER_DIS1, REGISTER_DIS2,
|
||||
REGISTER_REDOX, REGISTER_EC, REGISTER_OCI};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status) || registers.size() < 2)
|
||||
return;
|
||||
|
||||
void Kuntze::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||
// Each value is a register pair: the reading, then the number of decimal places in its low byte.
|
||||
float value = registers[0];
|
||||
for (uint16_t i = 0; i < (registers[1] & 0xFF); i++)
|
||||
value /= 10.0f;
|
||||
|
||||
this->waiting_ = false;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
|
||||
float value = (float) get_16bit(0);
|
||||
for (int i = 0; i < data[3]; i++)
|
||||
value /= 10.0;
|
||||
switch (this->state_) {
|
||||
case 1:
|
||||
switch (start_address) {
|
||||
case REGISTER_PH:
|
||||
ESP_LOGD(TAG, "pH=%.1f", value);
|
||||
if (this->ph_sensor_ != nullptr)
|
||||
this->ph_sensor_->publish_state(value);
|
||||
break;
|
||||
case 2:
|
||||
case REGISTER_TEMPERATURE:
|
||||
ESP_LOGD(TAG, "temperature=%.1f", value);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(value);
|
||||
break;
|
||||
case 3:
|
||||
case REGISTER_DIS1:
|
||||
ESP_LOGD(TAG, "DIS1=%.1f", value);
|
||||
if (this->dis1_sensor_ != nullptr)
|
||||
this->dis1_sensor_->publish_state(value);
|
||||
break;
|
||||
case 4:
|
||||
case REGISTER_DIS2:
|
||||
ESP_LOGD(TAG, "DIS2=%.1f", value);
|
||||
if (this->dis2_sensor_ != nullptr)
|
||||
this->dis2_sensor_->publish_state(value);
|
||||
break;
|
||||
case 5:
|
||||
case REGISTER_REDOX:
|
||||
ESP_LOGD(TAG, "REDOX=%.1f", value);
|
||||
if (this->redox_sensor_ != nullptr)
|
||||
this->redox_sensor_->publish_state(value);
|
||||
break;
|
||||
case 6:
|
||||
case REGISTER_EC:
|
||||
ESP_LOGD(TAG, "EC=%.1f", value);
|
||||
if (this->ec_sensor_ != nullptr)
|
||||
this->ec_sensor_->publish_state(value);
|
||||
break;
|
||||
case 7:
|
||||
case REGISTER_OCI:
|
||||
ESP_LOGD(TAG, "OCI=%.1f", value);
|
||||
if (this->oci_sensor_ != nullptr)
|
||||
this->oci_sensor_->publish_state(value);
|
||||
break;
|
||||
}
|
||||
if (++this->state_ > 7)
|
||||
this->state_ = 0;
|
||||
}
|
||||
|
||||
void Kuntze::loop() {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
// timeout after 15 seconds
|
||||
if (this->waiting_ && (now - this->last_send_ > 15000)) {
|
||||
ESP_LOGW(TAG, "timed out waiting for response");
|
||||
this->waiting_ = false;
|
||||
}
|
||||
if (this->waiting_ || (this->state_ == 0))
|
||||
return;
|
||||
this->last_send_ = now;
|
||||
this->read_holding_registers(REGISTER[this->state_ - 1], 2);
|
||||
this->waiting_ = true;
|
||||
void Kuntze::update() {
|
||||
for (uint16_t reg : REGISTER)
|
||||
this->read_holding_registers(reg, 2);
|
||||
}
|
||||
|
||||
void Kuntze::update() { this->state_ = 1; }
|
||||
|
||||
void Kuntze::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Kuntze:\n"
|
||||
|
||||
@@ -18,18 +18,14 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; }
|
||||
void set_oci_sensor(sensor::Sensor *oci_sensor) { oci_sensor_ = oci_sensor; }
|
||||
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
int state_{0};
|
||||
bool waiting_{false};
|
||||
uint32_t last_send_{0};
|
||||
|
||||
sensor::Sensor *ph_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *dis1_sensor_{nullptr};
|
||||
|
||||
@@ -5,7 +5,11 @@ namespace esphome::light {
|
||||
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
|
||||
if (this->gamma_table_ == nullptr)
|
||||
return value;
|
||||
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
|
||||
uint16_t table_value = progmem_read_uint16(&this->gamma_table_[value]);
|
||||
uint8_t result = (table_value + 128) / 257;
|
||||
if (result == 0 && table_value != 0)
|
||||
return 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TAG
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@FredM67"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
mk2pvrouter_ns = cg.esphome_ns.namespace("mk2pvrouter")
|
||||
Mk2PVRouter = mk2pvrouter_ns.class_("Mk2PVRouter", cg.Component, uart.UARTDevice)
|
||||
|
||||
CONF_MK2PVROUTER_ID = "mk2pvrouter_id"
|
||||
|
||||
# Tags are copied into a fixed-size buffer (MAX_TAG_SIZE = 8 in mk2pvrouter.h),
|
||||
# which needs room for a trailing null terminator.
|
||||
MAX_TAG_LEN = 7
|
||||
|
||||
MK2PVROUTER_LISTENER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_MK2PVROUTER_ID): cv.use_id(Mk2PVRouter),
|
||||
cv.Required(CONF_TAG): cv.All(
|
||||
cv.string_strict, cv.Length(min=1, max=MAX_TAG_LEN), lambda x: x.upper()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Mk2PVRouter),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> None:
|
||||
# Validate UART settings
|
||||
schema = uart.final_validate_device_schema(
|
||||
"mk2pvrouter",
|
||||
baud_rate=9600,
|
||||
parity="EVEN",
|
||||
data_bits=7,
|
||||
stop_bits=1,
|
||||
require_rx=True,
|
||||
require_tx=False,
|
||||
)
|
||||
schema(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
_request_listener_slot = cg.slot_counter("MK2PVROUTER_LISTENER_COUNT")
|
||||
|
||||
|
||||
async def register_mk2pvrouter_listener(mk2pvrouter: MockObj, var: MockObj) -> None:
|
||||
"""Register a listener with its hub and count it for the compile-time buffer size."""
|
||||
_request_listener_slot()
|
||||
cg.add(mk2pvrouter.register_mk2pvrouter_listener(var))
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "mk2pvrouter.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
static const char *const TAG = "mk2pvrouter";
|
||||
|
||||
constexpr uint8_t START_FRAME = 0x2;
|
||||
constexpr uint8_t END_FRAME = 0x3;
|
||||
constexpr uint8_t LINE_FEED = 0xa;
|
||||
constexpr uint8_t CARRIAGE_RETURN = 0xd;
|
||||
constexpr uint8_t TAB = 0x9;
|
||||
constexpr uint8_t MAX_ITERATIONS = 128;
|
||||
constexpr uint8_t CRC_MASK = 0x3F;
|
||||
constexpr uint8_t CRC_OFFSET = 0x20;
|
||||
|
||||
// Extracts a TAB-delimited field from [buf_start, buf_end) into dest.
|
||||
// Returns the field length, or 0 if no TAB was found, or the (uncopied) field
|
||||
// length if it's >= max_len.
|
||||
static size_t get_field(char *dest, const char *buf_start, const char *buf_end, size_t max_len) {
|
||||
const auto *const field_end = static_cast<const char *>(memchr(buf_start, TAB, buf_end - buf_start));
|
||||
if (!field_end)
|
||||
return 0;
|
||||
const size_t len = field_end - buf_start;
|
||||
if (len >= max_len) {
|
||||
ESP_LOGE(TAG, "Field too long: %zu bytes (max %zu)", len, max_len);
|
||||
return len;
|
||||
}
|
||||
|
||||
memcpy(dest, buf_start, len);
|
||||
dest[len] = '\0'; // Null-terminate
|
||||
return len;
|
||||
}
|
||||
|
||||
// Calculates the CRC (checksum) for a given group of characters.
|
||||
uint8_t Mk2PVRouter::calculate_crc_(const char *grp, size_t grp_len) {
|
||||
uint8_t crc_tmp{0};
|
||||
const auto effective_len = grp_len - CRC_SUFFIX_LEN;
|
||||
for (size_t i = 0; i < effective_len; i++) {
|
||||
crc_tmp += grp[i];
|
||||
}
|
||||
crc_tmp &= CRC_MASK;
|
||||
crc_tmp += CRC_OFFSET;
|
||||
return crc_tmp;
|
||||
}
|
||||
|
||||
// Verifies the CRC of a group against its trailing CRC byte.
|
||||
bool Mk2PVRouter::check_crc_(const char *grp, const char *grp_end) {
|
||||
const auto grp_len = grp_end - grp;
|
||||
if (grp_len < static_cast<decltype(grp_len)>(CRC_SUFFIX_LEN)) {
|
||||
ESP_LOGE(TAG, "Empty or too short group");
|
||||
return false;
|
||||
}
|
||||
const auto raw_crc = grp[grp_len - 1];
|
||||
|
||||
const auto calculated_crc = this->calculate_crc_(grp, grp_len);
|
||||
|
||||
if (raw_crc != calculated_crc) {
|
||||
ESP_LOGE(TAG, "CRC mismatch: expected %d, got %d", calculated_crc, raw_crc);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validates, parses, and publishes a single tag/value group.
|
||||
void Mk2PVRouter::process_group_(const char *grp, const char *grp_end) {
|
||||
if (!this->check_crc_(grp, grp_end))
|
||||
return;
|
||||
|
||||
size_t field_len = get_field(this->tag_, grp, grp_end, MAX_TAG_SIZE);
|
||||
if (!field_len || field_len >= MAX_TAG_SIZE) {
|
||||
ESP_LOGE(TAG, "Invalid tag");
|
||||
return;
|
||||
}
|
||||
const auto *val_start = grp + field_len + 1; // Skip tag + TAB.
|
||||
|
||||
field_len = get_field(this->val_, val_start, grp_end, MAX_VAL_SIZE);
|
||||
if (!field_len || field_len >= MAX_VAL_SIZE) {
|
||||
ESP_LOGE(TAG, "Invalid value for tag %s", this->tag_);
|
||||
return;
|
||||
}
|
||||
|
||||
this->publish_value_(this->tag_, this->val_);
|
||||
}
|
||||
|
||||
// Reads characters until `c` is found or the internal buffer is full.
|
||||
bool Mk2PVRouter::read_chars_until_(bool drop, uint8_t c) {
|
||||
size_t j{0};
|
||||
|
||||
while (this->available() > 0 && j++ < MAX_ITERATIONS) {
|
||||
const auto received = this->read();
|
||||
if (received < 0)
|
||||
continue;
|
||||
if (received == c)
|
||||
return true;
|
||||
if (drop)
|
||||
continue;
|
||||
if (this->buf_index_ >= (sizeof(this->buf_) - 1)) {
|
||||
ESP_LOGW(TAG, "Internal buffer full");
|
||||
this->buf_index_ = 0;
|
||||
this->state_ = State::WAITING_FOR_START;
|
||||
return false;
|
||||
}
|
||||
this->buf_[this->buf_index_++] = received;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Mk2PVRouter::loop() {
|
||||
switch (this->state_) {
|
||||
case State::WAITING_FOR_START:
|
||||
ESP_LOGVV(TAG, "State: WAITING_FOR_START");
|
||||
if (this->read_chars_until_(true, START_FRAME))
|
||||
this->state_ = State::START_FRAME_RECEIVED;
|
||||
break;
|
||||
case State::START_FRAME_RECEIVED:
|
||||
ESP_LOGVV(TAG, "State: START_FRAME_RECEIVED");
|
||||
if (this->read_chars_until_(false, END_FRAME))
|
||||
this->state_ = State::END_FRAME_RECEIVED;
|
||||
break;
|
||||
case State::END_FRAME_RECEIVED: {
|
||||
ESP_LOGVV(TAG, "State: END_FRAME_RECEIVED -> processing");
|
||||
|
||||
if (this->buf_index_ == 0) {
|
||||
this->state_ = State::WAITING_FOR_START;
|
||||
break;
|
||||
}
|
||||
|
||||
auto *buf_finger = this->buf_;
|
||||
auto *buf_end = this->buf_ + this->buf_index_;
|
||||
|
||||
// Each group: 0xa(LF) | Tag | 0x9(TAB) | Data | 0x9(TAB) | CRC | 0xd(CR)
|
||||
// CRC is computed over "Tag | TAB | Data | TAB".
|
||||
while ((buf_finger = static_cast<char *>(memchr(buf_finger, LINE_FEED, buf_end - buf_finger))) != nullptr) {
|
||||
++buf_finger; // Skip LF to the start of the group.
|
||||
|
||||
auto *const grp_end = static_cast<char *>(memchr(buf_finger, CARRIAGE_RETURN, buf_end - buf_finger));
|
||||
if (!grp_end) {
|
||||
ESP_LOGE(TAG, "No group found");
|
||||
break;
|
||||
}
|
||||
|
||||
this->process_group_(buf_finger, grp_end);
|
||||
|
||||
buf_finger = grp_end; // grp_end is always < buf_end, so this stays in bounds.
|
||||
}
|
||||
this->buf_index_ = 0;
|
||||
this->state_ = State::WAITING_FOR_START;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
for (auto *element : this->mk2pvrouter_listeners_) {
|
||||
if (strcmp(tag, element->get_tag()) != 0)
|
||||
continue;
|
||||
element->publish_val(val);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Mk2PVRouter::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
|
||||
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
|
||||
}
|
||||
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
|
||||
this->mk2pvrouter_listeners_.push_back(listener);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
/*
|
||||
* Buffer sizes based on the mk2pvrouter telemetry protocol, as implemented by the
|
||||
* firmware's teleinfo.h (see github.com/FredM67/PVRouter-{1,3}-phase):
|
||||
* - Tags: max 4 chars (S_MC is longest), most are 1-2 chars (P, V1, R2, etc.)
|
||||
* - Values: max 6 digits signed (-10000), typical 1-5 digits. Energy (E) is a daily
|
||||
* counter reset at midnight, so it stays well within 6 digits.
|
||||
* - Frame: STX + multiple lines (LF+tag+TAB+value+TAB+crc+CR) + ETX
|
||||
* - Line format: \n<tag>\t<value>\t<crc>\r (8-15 bytes per line)
|
||||
* - Multi-phase with all features: ~150-200 bytes
|
||||
*/
|
||||
static constexpr uint8_t MAX_TAG_SIZE = 8; // S_MC (4) + digit (1) + null (1) + margin (2)
|
||||
static constexpr uint8_t MAX_VAL_SIZE = 8; // -10000 (6) + null (1) + margin (1)
|
||||
static constexpr uint16_t MAX_BUF_SIZE = 256; // Full frame with all features enabled
|
||||
|
||||
// Listener interface for entities that want updates for a specific tag.
|
||||
class Mk2PVRouterListener {
|
||||
public:
|
||||
explicit Mk2PVRouterListener(const char *tag) : tag_(tag) {}
|
||||
virtual ~Mk2PVRouterListener() = default;
|
||||
const char *get_tag() const { return this->tag_; }
|
||||
virtual void publish_val(const char *val) = 0;
|
||||
|
||||
protected:
|
||||
const char *tag_;
|
||||
};
|
||||
|
||||
// Reads frames via UART, validates their CRC, and publishes tag/value pairs to listeners.
|
||||
class Mk2PVRouter final : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
void register_mk2pvrouter_listener(Mk2PVRouterListener *listener);
|
||||
#endif
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static constexpr size_t CRC_SUFFIX_LEN = 1;
|
||||
static constexpr uint32_t BAUD_RATE = 9600;
|
||||
|
||||
enum class State : uint8_t {
|
||||
WAITING_FOR_START,
|
||||
START_FRAME_RECEIVED,
|
||||
END_FRAME_RECEIVED,
|
||||
};
|
||||
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
StaticVector<Mk2PVRouterListener *, MK2PVROUTER_LISTENER_COUNT> mk2pvrouter_listeners_;
|
||||
#endif
|
||||
uint16_t buf_index_{0};
|
||||
State state_{State::WAITING_FOR_START};
|
||||
char tag_[MAX_TAG_SIZE];
|
||||
char val_[MAX_VAL_SIZE];
|
||||
char buf_[MAX_BUF_SIZE]; // Large buffer last to reduce padding
|
||||
|
||||
bool read_chars_until_(bool drop, uint8_t c);
|
||||
uint8_t calculate_crc_(const char *grp, size_t grp_len);
|
||||
bool check_crc_(const char *grp, const char *grp_end);
|
||||
void process_group_(const char *grp, const char *grp_end);
|
||||
void publish_value_(const char *tag, const char *val);
|
||||
};
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.const import CONF_ID, CONF_TAG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_MK2PVROUTER_ID,
|
||||
MK2PVROUTER_LISTENER_SCHEMA,
|
||||
mk2pvrouter_ns,
|
||||
register_mk2pvrouter_listener,
|
||||
)
|
||||
|
||||
Mk2PVRouterSensor = mk2pvrouter_ns.class_(
|
||||
"Mk2PVRouterSensor", sensor.Sensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(Mk2PVRouterSensor).extend(
|
||||
MK2PVROUTER_LISTENER_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
mk2pvrouter = await cg.get_variable(config[CONF_MK2PVROUTER_ID])
|
||||
await register_mk2pvrouter_listener(mk2pvrouter, var)
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "mk2pvrouter_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
static const char *const TAG = "mk2pvrouter_sensor";
|
||||
|
||||
Mk2PVRouterSensor::Mk2PVRouterSensor(const char *tag) : Mk2PVRouterListener(tag) {}
|
||||
|
||||
void Mk2PVRouterSensor::publish_val(const char *val) {
|
||||
auto result = parse_number<float>(val);
|
||||
if (!result.has_value()) {
|
||||
ESP_LOGW(TAG, "Failed to parse value '%s' for tag '%s'", val, this->get_tag());
|
||||
return;
|
||||
}
|
||||
this->publish_state(result.value());
|
||||
}
|
||||
|
||||
void Mk2PVRouterSensor::dump_config() {
|
||||
LOG_SENSOR(" ", "Mk2PVRouter Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Tag: %s", this->get_tag());
|
||||
}
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/mk2pvrouter/mk2pvrouter.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::mk2pvrouter {
|
||||
|
||||
class Mk2PVRouterSensor final : public Mk2PVRouterListener, public sensor::Sensor, public Component {
|
||||
public:
|
||||
explicit Mk2PVRouterSensor(const char *tag);
|
||||
void publish_val(const char *val) override;
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::mk2pvrouter
|
||||
@@ -292,25 +292,52 @@ std::optional<int64_t> payload_to_number(const uint8_t *data, size_t size, Senso
|
||||
}
|
||||
|
||||
std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) {
|
||||
const size_t required_size = required_payload_size(sensor_value_type);
|
||||
if (required_size == 0) {
|
||||
return 0; // RAW/unsupported: nothing to read
|
||||
// RAW and BIT carry no fixed-width number, so there is nothing to decode whatever the span holds.
|
||||
// register_width_for() reports 1 for them, so this must be checked before the width test below.
|
||||
if (sensor_value_type == SensorValueType::RAW || sensor_value_type == SensorValueType::BIT) {
|
||||
return 0;
|
||||
}
|
||||
const size_t required_words = required_size / 2;
|
||||
const uint16_t required_words = register_width_for(sensor_value_type);
|
||||
if (required_words > count) {
|
||||
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu",
|
||||
static_cast<unsigned int>(sensor_value_type), count, required_words);
|
||||
ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%u",
|
||||
static_cast<unsigned int>(sensor_value_type), count, static_cast<unsigned int>(required_words));
|
||||
return std::nullopt;
|
||||
}
|
||||
// Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the
|
||||
// sign-extension behaviour stays identical to the wire path.
|
||||
uint8_t bytes[8]; // at most 4 registers (QWORD)
|
||||
for (size_t i = 0; i < required_words; i++) {
|
||||
uint16_t reg = registers[i];
|
||||
bytes[i * 2] = static_cast<uint8_t>(reg >> 8);
|
||||
bytes[i * 2 + 1] = static_cast<uint8_t>(reg & 0xFF);
|
||||
// Registers are the wire's own unit, so decode them directly rather than serializing back to bytes.
|
||||
// Each case defers to registers_to_value() so the word order and sign rules have one definition, with
|
||||
// two deliberate exceptions matching what the byte decoder returned: the float types yield their bit
|
||||
// pattern rather than a float, and U_QWORD shares the signed branch because the return type is int64_t.
|
||||
switch (sensor_value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
return registers_to_value<SensorValueType::U_WORD>(registers);
|
||||
case SensorValueType::U_WORD_S:
|
||||
return registers_to_value<SensorValueType::U_WORD_S>(registers);
|
||||
case SensorValueType::S_WORD:
|
||||
return registers_to_value<SensorValueType::S_WORD>(registers);
|
||||
case SensorValueType::S_WORD_S:
|
||||
return registers_to_value<SensorValueType::S_WORD_S>(registers);
|
||||
case SensorValueType::U_DWORD:
|
||||
return registers_to_value<SensorValueType::U_DWORD>(registers);
|
||||
case SensorValueType::U_DWORD_R:
|
||||
return registers_to_value<SensorValueType::U_DWORD_R>(registers);
|
||||
case SensorValueType::S_DWORD:
|
||||
return registers_to_value<SensorValueType::S_DWORD>(registers);
|
||||
case SensorValueType::S_DWORD_R:
|
||||
return registers_to_value<SensorValueType::S_DWORD_R>(registers);
|
||||
case SensorValueType::FP32:
|
||||
return registers_to_uint32(registers[0], registers[1]);
|
||||
case SensorValueType::FP32_R:
|
||||
return registers_to_uint32(registers[1], registers[0]);
|
||||
// Signed for both: an unsigned QWORD above INT64_MAX has to come back as a negative int64_t.
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
return registers_to_value<SensorValueType::S_QWORD>(registers);
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
return registers_to_value<SensorValueType::S_QWORD_R>(registers);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
// Append a 16-bit value to a PDU in big-endian (wire) byte order.
|
||||
|
||||
@@ -229,7 +229,7 @@ inline bool value_type_is_float(SensorValueType v) {
|
||||
}
|
||||
|
||||
/// Number of 16-bit registers a value of this type occupies (RAW counts as one register).
|
||||
inline uint16_t register_width_for(SensorValueType v) {
|
||||
constexpr uint16_t register_width_for(SensorValueType v) {
|
||||
switch (v) {
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::S_DWORD:
|
||||
@@ -473,6 +473,87 @@ inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueTy
|
||||
*/
|
||||
std::optional<int64_t> registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type);
|
||||
|
||||
/// Combine two register words into a 32-bit value.
|
||||
constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) {
|
||||
return (static_cast<uint32_t>(high_word) << 16) | low_word;
|
||||
}
|
||||
|
||||
/// Combine four register words into a 64-bit value, most significant word first.
|
||||
constexpr uint64_t registers_to_uint64(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) {
|
||||
return (static_cast<uint64_t>(registers_to_uint32(word0, word1)) << 32) | registers_to_uint32(word2, word3);
|
||||
}
|
||||
|
||||
// Always false, whatever the type: it exists only to make the static_assert below depend on the
|
||||
// template argument. Not a queryable trait.
|
||||
template<SensorValueType> inline constexpr bool VALUE_TYPE_SUPPORTED = false;
|
||||
|
||||
/** Decode one value whose type is known at compile time, from registers in host byte order.
|
||||
* Unlike registers_to_number(), the type is a template argument, so only the one decode is compiled
|
||||
* and the caller gets the value's natural type back rather than an int64_t. The "_R" types take the
|
||||
* low word first; the rest take the high word first.
|
||||
* Supports every fixed-width type: the WORD, DWORD, QWORD and FP32 families, including their _S and
|
||||
* _R forms. RAW and BIT have no fixed width and fail to compile.
|
||||
* Use register_width_for() for the number of registers the caller must supply.
|
||||
* Note that the FP32 branches are only usable in a constant expression where std::bit_cast is
|
||||
* available; elsewhere bit_cast falls back to a non-constexpr memcpy (see core/helpers.h).
|
||||
*/
|
||||
template<SensorValueType VALUE_TYPE> constexpr auto registers_to_value(const uint16_t *registers) {
|
||||
if constexpr (VALUE_TYPE == SensorValueType::U_WORD) {
|
||||
return registers[0];
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_WORD) {
|
||||
return static_cast<int16_t>(registers[0]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::U_WORD_S) {
|
||||
return byteswap(registers[0]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_WORD_S) {
|
||||
return static_cast<int16_t>(byteswap(registers[0]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD) {
|
||||
return registers_to_uint32(registers[0], registers[1]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD_R) {
|
||||
return registers_to_uint32(registers[1], registers[0]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD) {
|
||||
return static_cast<int32_t>(registers_to_uint32(registers[0], registers[1]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) {
|
||||
return static_cast<int32_t>(registers_to_uint32(registers[1], registers[0]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::FP32) {
|
||||
return bit_cast<float>(registers_to_uint32(registers[0], registers[1]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) {
|
||||
return bit_cast<float>(registers_to_uint32(registers[1], registers[0]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD) {
|
||||
return registers_to_uint64(registers[0], registers[1], registers[2], registers[3]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD_R) {
|
||||
return registers_to_uint64(registers[3], registers[2], registers[1], registers[0]);
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD) {
|
||||
return static_cast<int64_t>(registers_to_uint64(registers[0], registers[1], registers[2], registers[3]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD_R) {
|
||||
return static_cast<int64_t>(registers_to_uint64(registers[3], registers[2], registers[1], registers[0]));
|
||||
} else {
|
||||
static_assert(VALUE_TYPE_SUPPORTED<VALUE_TYPE>, "registers_to_value() does not support this value type");
|
||||
}
|
||||
}
|
||||
|
||||
/// The type registers_to_value() yields for a given value type. Distinct from modbus::RegisterValues,
|
||||
/// which is a container of raw words.
|
||||
template<SensorValueType VALUE_TYPE>
|
||||
using RegisterValueType = decltype(registers_to_value<VALUE_TYPE>(static_cast<const uint16_t *>(nullptr)));
|
||||
|
||||
/** The value stored at an absolute register address, or nullopt when it is not wholly inside this
|
||||
* response. Lets a device decode by address rather than by offset, so a poll split across several
|
||||
* requests needs no extra bookkeeping: a value outside the response simply yields nullopt.
|
||||
* @param registers the response registers, in host byte order
|
||||
* @param start_address the address the response begins at
|
||||
* @param address the address of the wanted value
|
||||
*/
|
||||
template<SensorValueType VALUE_TYPE>
|
||||
constexpr std::optional<RegisterValueType<VALUE_TYPE>> value_at(std::span<const uint16_t> registers,
|
||||
uint16_t start_address, uint16_t address) {
|
||||
if (address < start_address)
|
||||
return std::nullopt;
|
||||
const size_t offset = static_cast<size_t>(address) - start_address;
|
||||
if (offset + register_width_for(VALUE_TYPE) > registers.size())
|
||||
return std::nullopt;
|
||||
return registers_to_value<VALUE_TYPE>(registers.data() + offset);
|
||||
}
|
||||
|
||||
/// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more.
|
||||
static constexpr uint16_t MAX_FEW_REGISTERS = 4;
|
||||
|
||||
|
||||
@@ -3,62 +3,64 @@
|
||||
|
||||
namespace esphome::pzemac {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
static const char *const TAG = "pzemac";
|
||||
|
||||
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
|
||||
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
|
||||
|
||||
void PZEMAC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < 20) {
|
||||
ESP_LOGW(TAG, "Invalid size for PZEM AC!");
|
||||
// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
|
||||
// 32-bit values are two registers, low word first.
|
||||
static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.1 V
|
||||
static const uint16_t PZEM_REGISTER_CURRENT = 1; // 2 registers, 0.001 A
|
||||
static const uint16_t PZEM_REGISTER_ACTIVE_POWER = 3; // 2 registers, 0.1 W
|
||||
static const uint16_t PZEM_REGISTER_ACTIVE_ENERGY = 5; // 2 registers, 1 Wh
|
||||
static const uint16_t PZEM_REGISTER_FREQUENCY = 7; // 1 register, 0.1 Hz
|
||||
static const uint16_t PZEM_REGISTER_POWER_FACTOR = 8; // 1 register, 0.01
|
||||
|
||||
void PZEMAC::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
// 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)
|
||||
return;
|
||||
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 {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
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);
|
||||
publish_2_registers(this->current_sensor_, PZEM_REGISTER_CURRENT, 1000.0f);
|
||||
publish_2_registers(this->power_sensor_, PZEM_REGISTER_ACTIVE_POWER, 10.0f);
|
||||
publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ACTIVE_ENERGY, 1.0f);
|
||||
publish_1_register(this->frequency_sensor_, PZEM_REGISTER_FREQUENCY, 10.0f);
|
||||
publish_1_register(this->power_factor_sensor_, PZEM_REGISTER_POWER_FACTOR, 100.0f);
|
||||
}
|
||||
|
||||
void PZEMAC::on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
modbus::ResponseStatus status) {
|
||||
// The only custom request this component sends is the energy reset; acknowledge its echo here so
|
||||
// the default unhandled-response warning stays meaningful.
|
||||
if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) {
|
||||
if (modbus::succeeded(status)) {
|
||||
ESP_LOGD(TAG, "Energy reset acknowledged");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Energy reset rejected");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
||||
// 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34
|
||||
// Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc--
|
||||
// 0 2 6 10 14 16
|
||||
|
||||
auto pzem_get_16bit = [&](size_t i) -> uint16_t {
|
||||
return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
|
||||
};
|
||||
auto pzem_get_32bit = [&](size_t i) -> uint32_t {
|
||||
return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
|
||||
};
|
||||
|
||||
uint16_t raw_voltage = pzem_get_16bit(0);
|
||||
float voltage = raw_voltage / 10.0f; // max 6553.5 V
|
||||
|
||||
uint32_t raw_current = pzem_get_32bit(2);
|
||||
float current = raw_current / 1000.0f; // max 4294967.295 A
|
||||
|
||||
uint32_t raw_active_power = pzem_get_32bit(6);
|
||||
float active_power = raw_active_power / 10.0f; // max 429496729.5 W
|
||||
|
||||
float active_energy = static_cast<float>(pzem_get_32bit(10));
|
||||
|
||||
uint16_t raw_frequency = pzem_get_16bit(14);
|
||||
float frequency = raw_frequency / 10.0f;
|
||||
|
||||
uint16_t raw_power_factor = pzem_get_16bit(16);
|
||||
float power_factor = raw_power_factor / 100.0f;
|
||||
|
||||
ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, E=%.1f Wh, F=%.1f Hz, PF=%.2f", voltage, current, active_power,
|
||||
active_energy, frequency, power_factor);
|
||||
if (this->voltage_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
if (this->current_sensor_ != nullptr)
|
||||
this->current_sensor_->publish_state(current);
|
||||
if (this->power_sensor_ != nullptr)
|
||||
this->power_sensor_->publish_state(active_power);
|
||||
if (this->energy_sensor_ != nullptr)
|
||||
this->energy_sensor_->publish_state(active_energy);
|
||||
if (this->frequency_sensor_ != nullptr)
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
if (this->power_factor_sensor_ != nullptr)
|
||||
this->power_factor_sensor_->publish_state(power_factor);
|
||||
modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
|
||||
}
|
||||
|
||||
void PZEMAC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); }
|
||||
|
||||
@@ -22,7 +22,10 @@ class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -3,55 +3,63 @@
|
||||
|
||||
namespace esphome::pzemdc {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
static const char *const TAG = "pzemdc";
|
||||
|
||||
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
|
||||
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
|
||||
static const uint8_t PZEM_REGISTER_COUNT = 8; // 8x 16-bit registers
|
||||
|
||||
void PZEMDC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < 16) {
|
||||
ESP_LOGW(TAG, "Invalid size for PZEM DC!");
|
||||
return;
|
||||
}
|
||||
// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
|
||||
// 32-bit values are two registers, low word first.
|
||||
static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.01 V
|
||||
static const uint16_t PZEM_REGISTER_CURRENT = 1; // 1 register, 0.01 A
|
||||
static const uint16_t PZEM_REGISTER_POWER = 2; // 2 registers, 0.1 W
|
||||
static const uint16_t PZEM_REGISTER_ENERGY = 4; // 2 registers, 1 Wh
|
||||
|
||||
// See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809
|
||||
// 0 1 2 3 4 5 6 7 = ModBus register
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index
|
||||
// 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29
|
||||
// Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc--
|
||||
void PZEMDC::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
auto pzem_get_16bit = [&](size_t i) -> uint16_t {
|
||||
return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
|
||||
};
|
||||
auto pzem_get_32bit = [&](size_t i) -> uint32_t {
|
||||
return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0);
|
||||
// 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)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value / divisor);
|
||||
};
|
||||
|
||||
uint16_t raw_voltage = pzem_get_16bit(0);
|
||||
float voltage = raw_voltage / 100.0f; // max 655.35 V
|
||||
auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, start_address, reg))
|
||||
sensor->publish_state(*value / divisor);
|
||||
};
|
||||
|
||||
uint16_t raw_current = pzem_get_16bit(2);
|
||||
float current = raw_current / 100.0f; // max 655.35 A
|
||||
|
||||
uint32_t raw_power = pzem_get_32bit(4);
|
||||
float power = raw_power / 10.0f; // max 429496729.5 W
|
||||
|
||||
uint32_t raw_energy = pzem_get_32bit(8);
|
||||
float energy = raw_energy / 1000.0f; // max 4294967.295 kWh
|
||||
|
||||
ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power);
|
||||
if (this->voltage_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
if (this->current_sensor_ != nullptr)
|
||||
this->current_sensor_->publish_state(current);
|
||||
if (this->power_sensor_ != nullptr)
|
||||
this->power_sensor_->publish_state(power);
|
||||
if (this->energy_sensor_ != nullptr)
|
||||
this->energy_sensor_->publish_state(energy);
|
||||
publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 100.0f);
|
||||
publish_1_register(this->current_sensor_, PZEM_REGISTER_CURRENT, 100.0f);
|
||||
publish_2_registers(this->power_sensor_, PZEM_REGISTER_POWER, 10.0f);
|
||||
publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ENERGY, 1000.0f);
|
||||
}
|
||||
|
||||
void PZEMDC::update() { this->read_input_registers(0, 8); }
|
||||
void PZEMDC::on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
modbus::ResponseStatus status) {
|
||||
// The only custom request this component sends is the energy reset; acknowledge its echo here so
|
||||
// the default unhandled-response warning stays meaningful.
|
||||
if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) {
|
||||
if (modbus::succeeded(status)) {
|
||||
ESP_LOGD(TAG, "Energy reset acknowledged");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Energy reset rejected");
|
||||
}
|
||||
return;
|
||||
}
|
||||
modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status);
|
||||
}
|
||||
|
||||
void PZEMDC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); }
|
||||
void PZEMDC::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"PZEMDC:\n"
|
||||
|
||||
@@ -18,7 +18,10 @@ class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -1,85 +1,48 @@
|
||||
#include "sdm_meter.h"
|
||||
#include "sdm_meter_registers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sdm_meter {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
static const char *const TAG = "sdm_meter";
|
||||
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 80 x 16-bit registers (40 float values)
|
||||
|
||||
void SDMMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for SDMMeter!");
|
||||
return;
|
||||
}
|
||||
void SDMMeter::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
auto sdm_meter_get_float = [&](size_t i) -> float {
|
||||
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
|
||||
float f;
|
||||
memcpy(&f, &temp, sizeof(f));
|
||||
return f;
|
||||
// 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) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
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++) {
|
||||
auto phase = this->phases_[i];
|
||||
auto &phase = this->phases_[i];
|
||||
if (!phase.setup)
|
||||
continue;
|
||||
|
||||
float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4));
|
||||
float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4));
|
||||
float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4));
|
||||
float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4));
|
||||
float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4));
|
||||
float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4));
|
||||
float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4));
|
||||
|
||||
ESP_LOGD(
|
||||
TAG,
|
||||
"SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f var, PF=%.3f, "
|
||||
"PA=%.3f °",
|
||||
i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle);
|
||||
if (phase.voltage_sensor_ != nullptr)
|
||||
phase.voltage_sensor_->publish_state(voltage);
|
||||
if (phase.current_sensor_ != nullptr)
|
||||
phase.current_sensor_->publish_state(current);
|
||||
if (phase.active_power_sensor_ != nullptr)
|
||||
phase.active_power_sensor_->publish_state(active_power);
|
||||
if (phase.apparent_power_sensor_ != nullptr)
|
||||
phase.apparent_power_sensor_->publish_state(apparent_power);
|
||||
if (phase.reactive_power_sensor_ != nullptr)
|
||||
phase.reactive_power_sensor_->publish_state(reactive_power);
|
||||
if (phase.power_factor_sensor_ != nullptr)
|
||||
phase.power_factor_sensor_->publish_state(power_factor);
|
||||
if (phase.phase_angle_sensor_ != nullptr)
|
||||
phase.phase_angle_sensor_->publish_state(phase_angle);
|
||||
publish(SDM_PHASE_1_VOLTAGE + i * 2, phase.voltage_sensor_);
|
||||
publish(SDM_PHASE_1_CURRENT + i * 2, phase.current_sensor_);
|
||||
publish(SDM_PHASE_1_ACTIVE_POWER + i * 2, phase.active_power_sensor_);
|
||||
publish(SDM_PHASE_1_APPARENT_POWER + i * 2, phase.apparent_power_sensor_);
|
||||
publish(SDM_PHASE_1_REACTIVE_POWER + i * 2, phase.reactive_power_sensor_);
|
||||
publish(SDM_PHASE_1_POWER_FACTOR + i * 2, phase.power_factor_sensor_);
|
||||
publish(SDM_PHASE_1_ANGLE + i * 2, phase.phase_angle_sensor_);
|
||||
}
|
||||
|
||||
float total_power = sdm_meter_get_float(SDM_TOTAL_SYSTEM_POWER * 2);
|
||||
float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2);
|
||||
float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2);
|
||||
float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2);
|
||||
float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2);
|
||||
float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2);
|
||||
|
||||
ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh, T.P=%.3f W",
|
||||
frequency, import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy,
|
||||
total_power);
|
||||
|
||||
if (this->total_power_sensor_ != nullptr)
|
||||
this->total_power_sensor_->publish_state(total_power);
|
||||
if (this->frequency_sensor_ != nullptr)
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
if (this->import_active_energy_sensor_ != nullptr)
|
||||
this->import_active_energy_sensor_->publish_state(import_active_energy);
|
||||
if (this->export_active_energy_sensor_ != nullptr)
|
||||
this->export_active_energy_sensor_->publish_state(export_active_energy);
|
||||
if (this->import_reactive_energy_sensor_ != nullptr)
|
||||
this->import_reactive_energy_sensor_->publish_state(import_reactive_energy);
|
||||
if (this->export_reactive_energy_sensor_ != nullptr)
|
||||
this->export_reactive_energy_sensor_->publish_state(export_reactive_energy);
|
||||
publish(SDM_TOTAL_SYSTEM_POWER, this->total_power_sensor_);
|
||||
publish(SDM_FREQUENCY, this->frequency_sensor_);
|
||||
publish(SDM_IMPORT_ACTIVE_ENERGY, this->import_active_energy_sensor_);
|
||||
publish(SDM_EXPORT_ACTIVE_ENERGY, this->export_active_energy_sensor_);
|
||||
publish(SDM_IMPORT_REACTIVE_ENERGY, this->import_reactive_energy_sensor_);
|
||||
publish(SDM_EXPORT_REACTIVE_ENERGY, this->export_reactive_energy_sensor_);
|
||||
}
|
||||
|
||||
void SDMMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); }
|
||||
|
||||
@@ -55,7 +55,8 @@ class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevic
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -1,84 +1,47 @@
|
||||
#include "selec_meter.h"
|
||||
#include "selec_meter_registers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
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
|
||||
|
||||
void SelecMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for SelecMeter!");
|
||||
return;
|
||||
}
|
||||
void SelecMeter::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
auto selec_meter_get_float = [&](size_t i, float unit) -> float {
|
||||
uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]);
|
||||
|
||||
float f;
|
||||
memcpy(&f, &temp, sizeof(f));
|
||||
return (f * unit);
|
||||
// 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.
|
||||
// Values are 32-bit floats, low word first.
|
||||
auto publish = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::FP32_R>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT);
|
||||
float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT);
|
||||
float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT);
|
||||
float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT);
|
||||
float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT);
|
||||
float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT);
|
||||
float maximum_demand_active_power =
|
||||
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
float maximum_demand_reactive_power =
|
||||
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
float maximum_demand_apparent_power =
|
||||
selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT);
|
||||
|
||||
if (this->total_active_energy_sensor_ != nullptr)
|
||||
this->total_active_energy_sensor_->publish_state(total_active_energy);
|
||||
if (this->import_active_energy_sensor_ != nullptr)
|
||||
this->import_active_energy_sensor_->publish_state(import_active_energy);
|
||||
if (this->export_active_energy_sensor_ != nullptr)
|
||||
this->export_active_energy_sensor_->publish_state(export_active_energy);
|
||||
if (this->total_reactive_energy_sensor_ != nullptr)
|
||||
this->total_reactive_energy_sensor_->publish_state(total_reactive_energy);
|
||||
if (this->import_reactive_energy_sensor_ != nullptr)
|
||||
this->import_reactive_energy_sensor_->publish_state(import_reactive_energy);
|
||||
if (this->export_reactive_energy_sensor_ != nullptr)
|
||||
this->export_reactive_energy_sensor_->publish_state(export_reactive_energy);
|
||||
if (this->apparent_energy_sensor_ != nullptr)
|
||||
this->apparent_energy_sensor_->publish_state(apparent_energy);
|
||||
if (this->active_power_sensor_ != nullptr)
|
||||
this->active_power_sensor_->publish_state(active_power);
|
||||
if (this->reactive_power_sensor_ != nullptr)
|
||||
this->reactive_power_sensor_->publish_state(reactive_power);
|
||||
if (this->apparent_power_sensor_ != nullptr)
|
||||
this->apparent_power_sensor_->publish_state(apparent_power);
|
||||
if (this->voltage_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
if (this->current_sensor_ != nullptr)
|
||||
this->current_sensor_->publish_state(current);
|
||||
if (this->power_factor_sensor_ != nullptr)
|
||||
this->power_factor_sensor_->publish_state(power_factor);
|
||||
if (this->frequency_sensor_ != nullptr)
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
if (this->maximum_demand_active_power_sensor_ != nullptr)
|
||||
this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power);
|
||||
if (this->maximum_demand_reactive_power_sensor_ != nullptr)
|
||||
this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power);
|
||||
if (this->maximum_demand_apparent_power_sensor_ != nullptr)
|
||||
this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power);
|
||||
publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->import_active_energy_sensor_, SELEC_IMPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->export_active_energy_sensor_, SELEC_EXPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->total_reactive_energy_sensor_, SELEC_TOTAL_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->import_reactive_energy_sensor_, SELEC_IMPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->export_reactive_energy_sensor_, SELEC_EXPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->apparent_energy_sensor_, SELEC_APPARENT_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->active_power_sensor_, SELEC_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->reactive_power_sensor_, SELEC_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->apparent_power_sensor_, SELEC_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->voltage_sensor_, SELEC_VOLTAGE, NO_DEC_UNIT);
|
||||
publish(this->current_sensor_, SELEC_CURRENT, NO_DEC_UNIT);
|
||||
publish(this->power_factor_sensor_, SELEC_POWER_FACTOR, NO_DEC_UNIT);
|
||||
publish(this->frequency_sensor_, SELEC_FREQUENCY, NO_DEC_UNIT);
|
||||
publish(this->maximum_demand_active_power_sensor_, SELEC_MAXIMUM_DEMAND_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->maximum_demand_reactive_power_sensor_, SELEC_MAXIMUM_DEMAND_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->maximum_demand_apparent_power_sensor_, SELEC_MAXIMUM_DEMAND_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
}
|
||||
|
||||
void SelecMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); }
|
||||
|
||||
@@ -37,7 +37,8 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
@@ -336,10 +336,8 @@ void log_update_interval(const char *tag, PollingComponent *component) {
|
||||
uint32_t update_interval = component->get_update_interval();
|
||||
if (update_interval == SCHEDULER_DONT_RUN) {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: never");
|
||||
} else if (update_interval < 100) {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f);
|
||||
} else {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f);
|
||||
ESP_LOGCONFIG(tag, " Update Interval: %" PRIu32 ".%03" PRIu32 "s", update_interval / 1000, update_interval % 1000);
|
||||
}
|
||||
}
|
||||
float Component::get_actual_setup_priority() const {
|
||||
|
||||
@@ -134,6 +134,7 @@
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 2
|
||||
#define MICRONOVA_LISTENER_COUNT 1
|
||||
#define USE_MICRONOVA_WRITER
|
||||
#define MK2PVROUTER_LISTENER_COUNT 1
|
||||
#define SERIAL_PROXY_COUNT 2
|
||||
#define SNTP_SERVER_COUNT 3
|
||||
#define USE_MEDIA_PLAYER
|
||||
|
||||
+24
-10
@@ -568,7 +568,7 @@ size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float
|
||||
}
|
||||
|
||||
// Fallback for NaN/Inf/high accuracy/out-of-range
|
||||
int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value);
|
||||
int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, static_cast<double>(value));
|
||||
if (len < 0)
|
||||
return 0;
|
||||
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
|
||||
@@ -586,16 +586,30 @@ size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> bu
|
||||
}
|
||||
|
||||
int8_t step_to_accuracy_decimals(float step) {
|
||||
// use printf %g to find number of digits based on temperature step
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof buf, "%.5g", step);
|
||||
|
||||
std::string str{buf};
|
||||
size_t dot_pos = str.find('.');
|
||||
if (dot_pos == std::string::npos)
|
||||
// Decimals needed to show the step at five significant digits, trailing zeros dropped.
|
||||
if (!std::isfinite(step) || step == 0.0f)
|
||||
return 0;
|
||||
|
||||
return str.length() - dot_pos - 1;
|
||||
float mantissa = std::fabs(step);
|
||||
int8_t decimals = 4; // decimals needed for five significant digits when mantissa is in [1, 10)
|
||||
while (mantissa >= 10.0f) {
|
||||
mantissa /= 10.0f;
|
||||
decimals--;
|
||||
}
|
||||
while (mantissa < 1.0f) {
|
||||
mantissa *= 10.0f;
|
||||
decimals++;
|
||||
}
|
||||
if (decimals <= 0)
|
||||
return 0;
|
||||
float scaled = mantissa * 10000.0f;
|
||||
auto digits = static_cast<uint32_t>(scaled);
|
||||
if (scaled - static_cast<float>(digits) >= 0.5f)
|
||||
digits++;
|
||||
while (decimals > 0 && digits % 10 == 0) {
|
||||
digits /= 10;
|
||||
decimals--;
|
||||
}
|
||||
return decimals;
|
||||
}
|
||||
|
||||
// Map a base64/base64url character to its 6-bit value (0-63) arithmetically.
|
||||
|
||||
Reference in New Issue
Block a user