Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-28 20:41:31 -05:00
40 changed files with 1202 additions and 532 deletions
+1
View File
@@ -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;
+28 -41
View File
@@ -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"
+2 -6
View File
@@ -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
+41 -14
View File
@@ -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.
+82 -1
View File
@@ -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;
+51 -49
View File
@@ -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); }
+4 -1
View File
@@ -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;
+46 -38
View File
@@ -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"
+4 -1
View File
@@ -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;
+28 -65
View File
@@ -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); }
+2 -1
View File
@@ -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;
+31 -68
View File
@@ -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); }
+2 -1
View File
@@ -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;
};
+1 -3
View File
@@ -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 {
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+46
View File
@@ -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}"
)