mirror of
https://github.com/esphome/esphome.git
synced 2026-08-30 17:46:01 +00:00
Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
This commit is contained in:
@@ -350,6 +350,7 @@ esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mitsubishi_cn105/* @crnjan
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
esphome/components/mk2pvrouter/* @FredM67
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
esphome/components/mlx90614/* @jesserockz
|
||||
esphome/components/mmc5603/* @benhoff
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.3.0
|
||||
resvg-py==0.4.0
|
||||
resvg-py==0.5.0
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==3.0.2
|
||||
|
||||
@@ -328,10 +328,10 @@ TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) {
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) {
|
||||
// %.5g prints these in exponent form, so the count comes from parsing "1e-05" or "1.2346e+05".
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 0);
|
||||
// %.5g would print these in exponent form; the count is now the real one rather than a parse of "1e-05".
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 5);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 8);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/light/esp_color_correction.h"
|
||||
|
||||
namespace esphome::light::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// A representative fixture for ESPColorCorrection/gamma_table_reverse_search tests below --
|
||||
// not a spec for generate_gamma_table() itself, which the Python tests own.
|
||||
std::array<uint16_t, 256> build_gamma_table(double gamma) {
|
||||
std::array<uint16_t, 256> table{};
|
||||
table[0] = 0;
|
||||
for (int i = 1; i < 256; i++) {
|
||||
double raw = std::round(std::pow(i / 255.0, gamma) * 65535.0);
|
||||
table[i] = static_cast<uint16_t>(std::max(1.0, std::min(65535.0, raw)));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
// Bundles a table with an ESPColorCorrection pointing at it, since the correction only holds
|
||||
// a raw pointer into the table and doesn't own it.
|
||||
struct GammaFixture {
|
||||
explicit GammaFixture(double gamma) : table(build_gamma_table(gamma)) { correction.set_gamma_table(table.data()); }
|
||||
std::array<uint16_t, 256> table;
|
||||
ESPColorCorrection correction;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Regression test for esphome/esphome#18842: ESPColorCorrection's own 16-bit -> 8-bit
|
||||
// conversion must never round a non-zero table entry down to a zero 8-bit output.
|
||||
TEST(GammaCorrection, NonZeroInputsSurviveConversion) {
|
||||
for (double gamma : {1.0, 1.8, 2.0, 2.2, 2.8, 3.0, 4.0}) {
|
||||
GammaFixture fixture(gamma);
|
||||
for (int i = 1; i < 256; i++) {
|
||||
EXPECT_GE(fixture.correction.color_correct_red(i), 1) << "gamma=" << gamma << " index=" << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GammaCorrection, ZeroInputStaysZero) {
|
||||
for (double gamma : {1.0, 2.2, 2.8, 4.0}) {
|
||||
GammaFixture fixture(gamma);
|
||||
EXPECT_EQ(fixture.correction.color_correct_red(0), 0) << "gamma=" << gamma;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GammaCorrection, FullBrightnessStaysFull) {
|
||||
for (double gamma : {1.0, 2.2, 2.8, 4.0}) {
|
||||
GammaFixture fixture(gamma);
|
||||
EXPECT_EQ(fixture.correction.color_correct_red(255), 255) << "gamma=" << gamma;
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduces the reporter's own numbers from esphome/esphome#18842 at gamma=2.8: codes
|
||||
// 1-27 previously collapsed to an 8-bit output of 0 and must now be non-zero.
|
||||
TEST(GammaCorrection, DeadZoneFixedAtGamma28) {
|
||||
GammaFixture fixture(2.8);
|
||||
for (int i = 1; i < 28; i++) {
|
||||
EXPECT_GE(fixture.correction.color_correct_red(i), 1) << "index=" << i << " still collapses to 0";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(GammaCorrection, ReverseSearchFindsLargestIndexLessEqualTarget) {
|
||||
auto table = build_gamma_table(2.8);
|
||||
for (uint16_t target : {0, 128, 129, 135, 1000, 32768, 65535}) {
|
||||
uint8_t lo = gamma_table_reverse_search(table.data(), target);
|
||||
EXPECT_LE(table[lo], target) << "target=" << target;
|
||||
if (lo < 255) {
|
||||
EXPECT_GT(table[lo + 1], target) << "target=" << target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// color_uncorrect_* binary-searches the table via gamma_table_reverse_search().
|
||||
TEST(GammaCorrection, UncorrectStaysMonotonic) {
|
||||
GammaFixture fixture(2.8);
|
||||
uint8_t prev = 0;
|
||||
for (int i = 1; i < 256; i++) {
|
||||
uint8_t result = fixture.correction.color_uncorrect_red(i);
|
||||
EXPECT_GE(result, prev) << "index=" << i;
|
||||
prev = result;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::light::testing
|
||||
@@ -0,0 +1,46 @@
|
||||
mk2pvrouter:
|
||||
id: test_mk2pvrouter
|
||||
uart_id: uart_bus
|
||||
|
||||
sensor:
|
||||
- platform: mk2pvrouter
|
||||
name: Power
|
||||
tag: P
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
unit_of_measurement: W
|
||||
device_class: power
|
||||
state_class: measurement
|
||||
accuracy_decimals: 0
|
||||
|
||||
- platform: mk2pvrouter
|
||||
name: Voltage
|
||||
tag: V
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
unit_of_measurement: V
|
||||
device_class: voltage
|
||||
state_class: measurement
|
||||
accuracy_decimals: 2
|
||||
filters:
|
||||
# Device sends voltage * 100
|
||||
- multiply: 0.01
|
||||
|
||||
- platform: mk2pvrouter
|
||||
name: Energy
|
||||
tag: E
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
unit_of_measurement: Wh
|
||||
device_class: energy
|
||||
state_class: total_increasing
|
||||
accuracy_decimals: 0
|
||||
|
||||
- platform: mk2pvrouter
|
||||
name: Temperature
|
||||
tag: T1
|
||||
mk2pvrouter_id: test_mk2pvrouter
|
||||
unit_of_measurement: "°C"
|
||||
device_class: temperature
|
||||
state_class: measurement
|
||||
accuracy_decimals: 2
|
||||
filters:
|
||||
# Device sends temperature * 100
|
||||
- multiply: 0.01
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml
|
||||
mk2pvrouter: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml
|
||||
mk2pvrouter: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml
|
||||
mk2pvrouter: !include common.yaml
|
||||
@@ -427,11 +427,132 @@ TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumberForQwords) {
|
||||
// The word shuffle the QWORD_R decode replaces is the least obvious code in the byte path, so pin
|
||||
// it against that path rather than against registers_to_value(). The top bit is set, which is where
|
||||
// U_QWORD's unsigned value and this function's int64_t return deliberately diverge.
|
||||
const uint16_t registers[] = {0xF123, 0x4567, 0x89AB, 0xCDEF};
|
||||
const std::vector<uint8_t> bytes{0xF1, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
|
||||
for (auto value_type :
|
||||
{SensorValueType::U_QWORD, SensorValueType::S_QWORD, SensorValueType::U_QWORD_R, SensorValueType::S_QWORD_R}) {
|
||||
EXPECT_EQ(registers_to_number(registers, 4, value_type),
|
||||
payload_to_number(std::span<const uint8_t>(bytes), value_type, 0, 0xFFFFFFFF))
|
||||
<< "value_type=" << static_cast<int>(value_type);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToNumberTreatsRawAndBitAsNothingToDecode) {
|
||||
// Both have no fixed-width number, so they decode to 0 whatever the span holds - including none.
|
||||
const uint16_t registers[] = {0x1234};
|
||||
EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::RAW), std::optional<int64_t>(0));
|
||||
EXPECT_EQ(registers_to_number(registers, 0, SensorValueType::RAW), std::optional<int64_t>(0));
|
||||
EXPECT_EQ(registers_to_number(registers, 0, SensorValueType::BIT), std::optional<int64_t>(0));
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) {
|
||||
const uint16_t registers[] = {0x1234};
|
||||
EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value());
|
||||
}
|
||||
|
||||
// --- registers_to_value ----------------------------------------------------
|
||||
// registers_to_number() dispatches to registers_to_value(), so this checks the dispatch table picks
|
||||
// the right specialisation for each type, not that two implementations agree. The independent check
|
||||
// against the byte decoder is RegistersToNumberMatchesPayloadToNumber below.
|
||||
|
||||
template<SensorValueType VALUE_TYPE> void expect_matches_registers_to_number(const uint16_t *registers) {
|
||||
const auto expected = registers_to_number(registers, register_width_for(VALUE_TYPE), VALUE_TYPE);
|
||||
// Plain control flow rather than ASSERT_TRUE: the optional analysis does not see through the macro.
|
||||
if (!expected.has_value()) {
|
||||
ADD_FAILURE() << "registers_to_number() returned no value for value_type=" << static_cast<int>(VALUE_TYPE);
|
||||
return;
|
||||
}
|
||||
const int64_t number = expected.value();
|
||||
if constexpr (VALUE_TYPE == SensorValueType::FP32 || VALUE_TYPE == SensorValueType::FP32_R) {
|
||||
EXPECT_FLOAT_EQ(registers_to_value<VALUE_TYPE>(registers), bit_cast<float>(static_cast<uint32_t>(number)))
|
||||
<< "value_type=" << static_cast<int>(VALUE_TYPE);
|
||||
} else {
|
||||
EXPECT_EQ(static_cast<int64_t>(registers_to_value<VALUE_TYPE>(registers)), number)
|
||||
<< "value_type=" << static_cast<int>(VALUE_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToValueMatchesRegistersToNumber) {
|
||||
// A high bit in each word exercises sign handling and word order together.
|
||||
const uint16_t registers[] = {0x8001, 0xFE02};
|
||||
expect_matches_registers_to_number<SensorValueType::U_WORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_WORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_WORD_S>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_WORD_S>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_DWORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_DWORD_R>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_DWORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_DWORD_R>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::FP32>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::FP32_R>(registers);
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToUint32CombinesWordsHighFirst) {
|
||||
EXPECT_EQ(registers_to_uint32(0x1234, 0x5678), 0x12345678u);
|
||||
}
|
||||
|
||||
// --- value_at ---------------------------------------------------------------
|
||||
// Addresses are absolute; anything not wholly inside the response yields nullopt.
|
||||
|
||||
TEST(ModbusHelpersTest, ValueAtDecodesByAbsoluteAddress) {
|
||||
const uint16_t registers[] = {0x1111, 0x2222, 0x3333};
|
||||
const std::span<const uint16_t> span(registers, 3);
|
||||
EXPECT_EQ(value_at<SensorValueType::U_WORD>(span, 100, 100), std::optional<uint16_t>(0x1111));
|
||||
EXPECT_EQ(value_at<SensorValueType::U_WORD>(span, 100, 102), std::optional<uint16_t>(0x3333));
|
||||
EXPECT_EQ(value_at<SensorValueType::U_DWORD>(span, 100, 101), std::optional<uint32_t>(0x22223333u));
|
||||
// Types whose RegisterValueType<> is not an unsigned integer, and the widest bounds check.
|
||||
const uint16_t floats[] = {0x4048, 0xF5C3, 0xF5C3, 0x4048};
|
||||
const std::span<const uint16_t> float_span(floats, 4);
|
||||
EXPECT_FLOAT_EQ(value_at<SensorValueType::FP32>(float_span, 10, 10).value_or(0.0f), 3.14f);
|
||||
EXPECT_FLOAT_EQ(value_at<SensorValueType::FP32_R>(float_span, 10, 12).value_or(0.0f), 3.14f);
|
||||
EXPECT_EQ(value_at<SensorValueType::U_QWORD>(float_span, 10, 10), std::optional<uint64_t>(0x4048F5C3F5C34048ULL));
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_QWORD>(float_span, 10, 11).has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, ValueAtIsUsableInAConstantExpression) {
|
||||
static constexpr uint16_t REGISTERS[] = {0x1234, 0x5678};
|
||||
static_assert(value_at<SensorValueType::U_DWORD>(REGISTERS, 7, 7).value_or(0) == 0x12345678u);
|
||||
static_assert(!value_at<SensorValueType::U_DWORD>(REGISTERS, 7, 6).has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, ValueAtRejectsAddressesOutsideTheResponse) {
|
||||
const uint16_t registers[] = {0x1111, 0x2222, 0x3333};
|
||||
const std::span<const uint16_t> span(registers, 3);
|
||||
// Below the response: must not wrap when the subtraction would go negative.
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_WORD>(span, 100, 99).has_value());
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_WORD>(span, 100, 0).has_value());
|
||||
// Past the end, and a multi-register value truncated by the end of the response.
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_WORD>(span, 100, 103).has_value());
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_DWORD>(span, 100, 102).has_value());
|
||||
EXPECT_TRUE(value_at<SensorValueType::U_DWORD>(span, 100, 101).has_value());
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, ValueAtHandlesAnEmptyResponse) {
|
||||
EXPECT_FALSE(value_at<SensorValueType::U_WORD>(std::span<const uint16_t>(), 0, 0).has_value());
|
||||
}
|
||||
|
||||
// --- QWORD decoding ---------------------------------------------------------
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToValueDecodesQwordBothWordOrders) {
|
||||
const uint16_t registers[] = {0x0123, 0x4567, 0x89AB, 0xCDEF};
|
||||
EXPECT_EQ(registers_to_value<SensorValueType::U_QWORD>(registers), 0x0123456789ABCDEFULL);
|
||||
const uint16_t reversed[] = {0xCDEF, 0x89AB, 0x4567, 0x0123};
|
||||
EXPECT_EQ(registers_to_value<SensorValueType::U_QWORD_R>(reversed), 0x0123456789ABCDEFULL);
|
||||
// Signed reading of the same bits, and the sign-extreme case.
|
||||
EXPECT_EQ(registers_to_value<SensorValueType::S_QWORD>(registers), 0x0123456789ABCDEFLL);
|
||||
const uint16_t negative[] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFE};
|
||||
EXPECT_EQ(registers_to_value<SensorValueType::S_QWORD>(negative), -2);
|
||||
EXPECT_EQ(registers_to_value<SensorValueType::U_QWORD>(negative), 0xFFFFFFFFFFFFFFFEULL);
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToUint64CombinesWordsHighFirst) {
|
||||
EXPECT_EQ(registers_to_uint64(0x0123, 0x4567, 0x89AB, 0xCDEF), 0x0123456789ABCDEFULL);
|
||||
}
|
||||
|
||||
// --- packed bit helpers ------------------------------------------------------
|
||||
|
||||
TEST(ModbusHelpersTest, PackBitsAppendsToContainer) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Common UART configuration for ESP32 Arduino tests - 9600 baud, EVEN parity, 7 data bits
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 9600
|
||||
parity: EVEN
|
||||
data_bits: 7
|
||||
stop_bits: 1
|
||||
@@ -0,0 +1,14 @@
|
||||
# Common UART configuration for ESP32 IDF tests - 9600 baud, EVEN parity, 7 data bits
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 9600
|
||||
parity: EVEN
|
||||
data_bits: 7
|
||||
stop_bits: 1
|
||||
@@ -0,0 +1,14 @@
|
||||
# Common UART configuration for ESP8266 Arduino tests - 9600 baud even parity, 7 data bits
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 9600
|
||||
parity: EVEN
|
||||
data_bits: 7
|
||||
stop_bits: 1
|
||||
@@ -0,0 +1,14 @@
|
||||
# Common UART configuration for RP2040 Arduino tests - 9600 baud even parity, 7 data bits
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO1
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 9600
|
||||
parity: EVEN
|
||||
data_bits: 7
|
||||
stop_bits: 1
|
||||
@@ -53,9 +53,12 @@ def test_nonzero_indices_are_nonzero(gamma: float) -> None:
|
||||
assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0])
|
||||
@pytest.mark.parametrize("gamma", [1.0, 1.8, 2.0, 2.2, 2.8, 3.0, 4.0])
|
||||
def test_table_monotonically_nondecreasing(gamma: float) -> None:
|
||||
"""The gamma table must be monotonically non-decreasing."""
|
||||
"""The gamma table must be monotonically non-decreasing.
|
||||
|
||||
gamma_table_reverse_search()'s binary search depends on this.
|
||||
"""
|
||||
table = generate_gamma_table(gamma)
|
||||
for i in range(1, 256):
|
||||
assert table[i] >= table[i - 1], (
|
||||
@@ -115,3 +118,13 @@ def test_lut_output_monotonically_nondecreasing() -> None:
|
||||
result = _simulate_gamma_correct_lut(table, value)
|
||||
assert result >= prev, f"value={value}: result {result} < previous {prev}"
|
||||
prev = result
|
||||
|
||||
|
||||
def test_table_matches_raw_power_curve() -> None:
|
||||
"""Check the gamma table against known good values for gamma=2.8."""
|
||||
table = generate_gamma_table(2.8)
|
||||
golden = {1: 1, 5: 1, 15: 24, 27: 122, 28: 135, 100: 4766, 200: 33193, 254: 64818}
|
||||
for i, expected in golden.items():
|
||||
assert table[i] == expected, (
|
||||
f"index {i}: table[{i}]={table[i]} expected {expected}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user