From e94e71ded80b5d6b5c943fce0f08d1f44a7ebe62 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Thu, 8 May 2025 20:50:59 -0400 Subject: [PATCH] ATM90E32 Semi-automatic calibration & Status fields (#8529) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/atm90e32/__init__.py | 1 + esphome/components/atm90e32/atm90e32.cpp | 781 +++++++++++++----- esphome/components/atm90e32/atm90e32.h | 159 +++- esphome/components/atm90e32/atm90e32_reg.h | 6 +- .../components/atm90e32/button/__init__.py | 72 +- .../atm90e32/button/atm90e32_button.cpp | 67 +- .../atm90e32/button/atm90e32_button.h | 40 +- .../components/atm90e32/number/__init__.py | 130 +++ .../atm90e32/number/atm90e32_number.h | 16 + esphome/components/atm90e32/sensor.py | 25 +- .../atm90e32/text_sensor/__init__.py | 48 ++ tests/components/atm90e32/common.yaml | 84 +- 12 files changed, 1168 insertions(+), 261 deletions(-) create mode 100644 esphome/components/atm90e32/number/__init__.py create mode 100644 esphome/components/atm90e32/number/atm90e32_number.h create mode 100644 esphome/components/atm90e32/text_sensor/__init__.py diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index 8ce95be489..766807872b 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -3,5 +3,6 @@ import esphome.codegen as cg CODEOWNERS = ["@circuitsetup", "@descipher"] atm90e32_ns = cg.esphome_ns.namespace("atm90e32") +ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component) CONF_ATM90E32_ID = "atm90e32_id" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 43647b1855..f4f177587c 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,7 +1,7 @@ #include "atm90e32.h" -#include "atm90e32_reg.h" -#include "esphome/core/log.h" #include +#include +#include "esphome/core/log.h" namespace esphome { namespace atm90e32 { @@ -11,115 +11,84 @@ void ATM90E32Component::loop() { if (this->get_publish_interval_flag_()) { this->set_publish_interval_flag_(false); for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].voltage_sensor_ != nullptr) { + if (this->phase_[phase].voltage_sensor_ != nullptr) this->phase_[phase].voltage_ = this->get_phase_voltage_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].current_sensor_ != nullptr) { + + if (this->phase_[phase].current_sensor_ != nullptr) this->phase_[phase].current_ = this->get_phase_current_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_sensor_ != nullptr) { + + if (this->phase_[phase].power_sensor_ != nullptr) this->phase_[phase].active_power_ = this->get_phase_active_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_factor_sensor_ != nullptr) { + + if (this->phase_[phase].power_factor_sensor_ != nullptr) this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + + if (this->phase_[phase].reactive_power_sensor_ != nullptr) this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { + + if (this->phase_[phase].apparent_power_sensor_ != nullptr) + this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase); + + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { + + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + + if (this->phase_[phase].phase_angle_sensor_ != nullptr) this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { + + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].peak_current_sensor_ != nullptr) { + + if (this->phase_[phase].peak_current_sensor_ != nullptr) this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase); - } - } - // After the local store in collected we can publish them trusting they are withing +-1 haardware sampling - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].voltage_sensor_ != nullptr) { + + // After the local store is collected we can publish them trusting they are within +-1 hardware sampling + if (this->phase_[phase].voltage_sensor_ != nullptr) this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].current_sensor_ != nullptr) { + + if (this->phase_[phase].current_sensor_ != nullptr) this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_sensor_ != nullptr) { + + if (this->phase_[phase].power_sensor_ != nullptr) this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].power_factor_sensor_ != nullptr) { + + if (this->phase_[phase].power_factor_sensor_ != nullptr) this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].reactive_power_sensor_ != nullptr) { + + if (this->phase_[phase].reactive_power_sensor_ != nullptr) this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { + + if (this->phase_[phase].apparent_power_sensor_ != nullptr) + this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase)); + if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { this->phase_[phase].forward_active_energy_sensor_->publish_state( this->get_local_phase_forward_active_energy_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { this->phase_[phase].reverse_active_energy_sensor_->publish_state( this->get_local_phase_reverse_active_energy_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].phase_angle_sensor_ != nullptr) { + + if (this->phase_[phase].phase_angle_sensor_ != nullptr) this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase)); - } - } - for (uint8_t phase = 0; phase < 3; phase++) { + if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { this->phase_[phase].harmonic_active_power_sensor_->publish_state( this->get_local_phase_harmonic_active_power_(phase)); } - } - for (uint8_t phase = 0; phase < 3; phase++) { - if (this->phase_[phase].peak_current_sensor_ != nullptr) { + + if (this->phase_[phase].peak_current_sensor_ != nullptr) this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase)); - } } - if (this->freq_sensor_ != nullptr) { + if (this->freq_sensor_ != nullptr) this->freq_sensor_->publish_state(this->get_frequency_()); - } - if (this->chip_temperature_sensor_ != nullptr) { + + if (this->chip_temperature_sensor_ != nullptr) this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); - } } } @@ -130,82 +99,30 @@ void ATM90E32Component::update() { } this->set_publish_interval_flag_(true); this->status_clear_warning(); -} -void ATM90E32Component::restore_calibrations_() { - if (enable_offset_calibration_) { - this->pref_.load(&this->offset_phase_); - } -}; - -void ATM90E32Component::run_offset_calibrations() { - // Run the calibrations and - // Setup voltage and current calibration offsets for PHASE A - this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); - this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); - this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset - // Setup voltage and current calibration offsets for PHASE B - this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); - this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); - this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset - // Setup voltage and current calibration offsets for PHASE C - this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); - this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); - this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset - this->pref_.save(&this->offset_phase_); - ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, - this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); - ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, - this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); -} - -void ATM90E32Component::clear_offset_calibrations() { - // Clear the calibrations and - this->offset_phase_[PHASEA].voltage_offset_ = 0; - this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEA].current_offset_ = 0; - this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset - this->offset_phase_[PHASEB].voltage_offset_ = 0; - this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEB].current_offset_ = 0; - this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset - this->offset_phase_[PHASEC].voltage_offset_ = 0; - this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->offset_phase_[PHASEC].current_offset_ = 0; - this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset - this->pref_.save(&this->offset_phase_); - ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, - this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); - ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, - this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +#ifdef USE_TEXT_SENSOR + this->check_phase_status(); + this->check_over_current(); + this->check_freq_status(); +#endif } void ATM90E32Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - if (this->enable_offset_calibration_) { - uint32_t hash = fnv1_hash(App.get_friendly_name()); - this->pref_ = global_preferences->make_preference(hash, true); - this->restore_calibrations_(); - } + uint16_t mmode0 = 0x87; // 3P4W 50Hz + uint16_t high_thresh = 0; + uint16_t low_thresh = 0; + if (line_freq_ == 60) { mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz + // for freq threshold registers + high_thresh = 6300; // 63.00 Hz + low_thresh = 5700; // 57.00 Hz + } else { + high_thresh = 5300; // 53.00 Hz + low_thresh = 4700; // 47.00 Hz } if (current_phases_ == 2) { @@ -216,34 +133,84 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset delay(6); // Wait for the minimum 5ms + 1ms this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) { + if (!this->validate_spi_read_(0x55AA, "setup()")) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); return; } this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering - this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0) + this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) - this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold + this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% - // Setup voltage and current gain for PHASE A - this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain - // Setup voltage and current gain for PHASE B - this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain - // Setup voltage and current gain for PHASE C - this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain - this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration + + if (this->enable_offset_calibration_) { + // Initialize flash storage for offset calibrations + uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary()); + this->offset_pref_ = global_preferences->make_preference(o_hash, true); + this->restore_offset_calibrations_(); + + // Initialize flash storage for power offset calibrations + uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary()); + this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); + this->restore_power_offset_calibrations_(); + } else { + ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values."); + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(this->voltage_offset_registers[phase], + static_cast(this->offset_phase_[phase].voltage_offset_)); + this->write16_(this->current_offset_registers[phase], + static_cast(this->offset_phase_[phase].current_offset_)); + this->write16_(this->power_offset_registers[phase], + static_cast(this->power_offset_phase_[phase].active_power_offset)); + this->write16_(this->reactive_power_offset_registers[phase], + static_cast(this->power_offset_phase_[phase].reactive_power_offset)); + } + } + + if (this->enable_gain_calibration_) { + // Initialize flash storage for gain calibration + uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary()); + this->gain_calibration_pref_ = global_preferences->make_preference(g_hash, true); + this->restore_gain_calibrations_(); + + if (this->using_saved_calibrations_) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory."); + } else { + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); + this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); + } + } + } else { + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values."); + + for (uint8_t phase = 0; phase < 3; ++phase) { + this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); + this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); + } + } + + // Sag threshold (78%) + uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f); + // Overvoltage threshold (122%) + uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f); + + // Write to registers + this->write16_(ATM90E32_REGISTER_SAGTH, sagth); + this->write16_(ATM90E32_REGISTER_OVTH, ovth); + + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } void ATM90E32Component::dump_config() { @@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_); @@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_); - LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_); - LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_); + LOG_SENSOR(" ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_); LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_); - LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_); - LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_); - LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_); + LOG_SENSOR(" ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_); + LOG_SENSOR(" ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_); + LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } @@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) { uint8_t data[2]; uint16_t output; this->enable(); - delay_microseconds_safe(10); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty this->write_byte(addrh); this->write_byte(addrl); this->read_array(data, 2); @@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { this->write_byte16(a_register); this->write_byte16(val); this->disable(); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val) - ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val); + this->validate_spi_read_(val, "write16()"); } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } @@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; } +float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; } + float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; } float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) { @@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t float ATM90E32Component::get_phase_voltage_(uint8_t phase) { const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) - ESP_LOGW(TAG, "SPI URMS voltage register read error."); + this->validate_spi_read_(voltage, "get_phase_voltage()"); return (float) voltage / 100; } @@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) { uint16_t voltage = 0; for (uint8_t i = 0; i < reads; i++) { voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) - ESP_LOGW(TAG, "SPI URMS voltage register read error."); + this->validate_spi_read_(voltage, "get_phase_voltage_avg_()"); accumulation += voltage; } voltage = accumulation / reads; @@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) { uint16_t current = 0; for (uint8_t i = 0; i < reads; i++) { current = this->read16_(ATM90E32_REGISTER_IRMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) - ESP_LOGW(TAG, "SPI IRMS current register read error."); + this->validate_spi_read_(current, "get_phase_current_avg_()"); accumulation += current; } current = accumulation / reads; @@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) { float ATM90E32Component::get_phase_current_(uint8_t phase) { const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) - ESP_LOGW(TAG, "SPI IRMS current register read error."); + this->validate_spi_read_(current, "get_phase_current_()"); return (float) current / 1000; } @@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) { return val * 0.00032f; } +float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) { + const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase); + return val * 0.00032f; +} + float ATM90E32Component::get_phase_power_factor_(uint8_t phase) { - const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); - if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor) - ESP_LOGW(TAG, "SPI power factor read error."); - return (float) powerfactor / 1000; + uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); // unsigned to compare to lastspidata + this->validate_spi_read_(powerfactor, "get_phase_power_factor_()"); + return (float) ((int16_t) powerfactor) / 1000; // make it signed again } float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { @@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { } else { this->phase_[phase].cumulative_forward_active_energy_ = val; } - return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200); + // 0.01CF resolution = 0.003125 Wh per count + return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f)); } float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) { - const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY); + const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase); if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) { this->phase_[phase].cumulative_reverse_active_energy_ += val; } else { this->phase_[phase].cumulative_reverse_active_energy_ = val; } - return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200); + // 0.01CF resolution = 0.003125 Wh per count + return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f)); } float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { @@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { float ATM90E32Component::get_phase_angle_(uint8_t phase) { uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; - return (float) (val > 180) ? val - 360.0 : val; + return (val > 180) ? (float) (val - 360.0f) : (float) val; } float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase); if (!this->peak_current_signed_) - val = abs(val); + val = std::abs(val); // phase register * phase current gain value / 1000 * 2^13 - return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0); + return (val * this->phase_[phase].ct_gain_ / 8192000.0); } float ATM90E32Component::get_frequency_() { @@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() { return (float) ctemp; } -uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) { - const uint8_t num_reads = 5; - uint64_t total_value = 0; - for (int i = 0; i < num_reads; ++i) { - const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase); - total_value += measurement_value; +void ATM90E32Component::run_gain_calibrations() { + if (!this->enable_gain_calibration_) { + ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); + return; } - const uint32_t average_value = total_value / num_reads; - const uint32_t shifted_value = average_value >> 7; - const uint32_t voltage_offset = ~shifted_value + 1; - return voltage_offset & 0xFFFF; // Take the lower 16 bits + + float ref_voltages[3] = { + this->get_reference_voltage(0), + this->get_reference_voltage(1), + this->get_reference_voltage(2), + }; + float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1), + this->get_reference_current(2)}; + + ESP_LOGI(TAG, "[CALIBRATION] "); + ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration ========================="); + ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + ESP_LOGI(TAG, + "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |"); + ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + + for (uint8_t phase = 0; phase < 3; phase++) { + float measured_voltage = this->get_phase_voltage_avg_(phase); + float measured_current = this->get_phase_current_avg_(phase); + + float ref_voltage = ref_voltages[phase]; + float ref_current = ref_currents[phase]; + + uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]); + uint16_t current_current_gain = this->read16_(current_gain_registers[phase]); + + bool did_voltage = false; + bool did_current = false; + + // Voltage calibration + if (ref_voltage <= 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", + phase_labels[phase]); + } else if (measured_voltage == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", + phase_labels[phase]); + } else { + uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); + if (new_voltage_gain == 0) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", + phase_labels[phase]); + } else { + if (new_voltage_gain >= 65535) { + ESP_LOGW( + TAG, + "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.", + phase_labels[phase]); + new_voltage_gain = 65535; + } + this->gain_phase_[phase].voltage_gain = static_cast(new_voltage_gain); + did_voltage = true; + } + } + + // Current calibration + if (ref_current == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", + phase_labels[phase]); + } else if (measured_current == 0.0f) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", + phase_labels[phase]); + } else { + uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); + if (new_current_gain == 0) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.", + phase_labels[phase]); + } else { + if (new_current_gain >= 65535) { + ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", + phase_labels[phase]); + new_current_gain = 65535; + } + this->gain_phase_[phase].current_gain = static_cast(new_current_gain); + did_current = true; + } + } + + // Final row output + ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", + 'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain, + did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain, + did_current ? this->gain_phase_[phase].current_gain : current_current_gain); + } + + ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n"); + + this->save_gain_calibration_to_memory_(); + this->write_gains_to_registers_(); + this->verify_gain_writes_(); } -uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { +void ATM90E32Component::save_gain_calibration_to_memory_() { + bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + if (success) { + this->using_saved_calibrations_ = true; + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); + } +} + +void ATM90E32Component::run_offset_calibrations() { + if (!this->enable_offset_calibration_) { + ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); + return; + } + + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t voltage_offset = calibrate_offset(phase, true); + int16_t current_offset = calibrate_offset(phase, false); + + this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset, + current_offset); + } + + this->offset_pref_.save(&this->offset_phase_); // Save to flash +} + +void ATM90E32Component::run_power_offset_calibrations() { + if (!this->enable_offset_calibration_) { + ESP_LOGW( + TAG, + "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); + return; + } + + for (uint8_t phase = 0; phase < 3; ++phase) { + int16_t active_offset = calibrate_power_offset(phase, false); + int16_t reactive_offset = calibrate_power_offset(phase, true); + + this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, + active_offset, reactive_offset); + } + + this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash +} + +void ATM90E32Component::write_gains_to_registers_() { + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + + for (int phase = 0; phase < 3; phase++) { + this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain); + this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain); + } + + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) { + // Save to runtime + this->offset_phase_[phase].voltage_offset_ = voltage_offset; + this->phase_[phase].voltage_offset_ = voltage_offset; + + // Save to flash-storable struct + this->offset_phase_[phase].current_offset_ = current_offset; + this->phase_[phase].current_offset_ = current_offset; + + // Write to registers + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + this->write16_(voltage_offset_registers[phase], static_cast(voltage_offset)); + this->write16_(current_offset_registers[phase], static_cast(current_offset)); + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) { + // Save to runtime + this->phase_[phase].active_power_offset_ = p_offset; + this->phase_[phase].reactive_power_offset_ = q_offset; + + // Save to flash-storable struct + this->power_offset_phase_[phase].active_power_offset = p_offset; + this->power_offset_phase_[phase].reactive_power_offset = q_offset; + + // Write to registers + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); + this->write16_(this->power_offset_registers[phase], static_cast(p_offset)); + this->write16_(this->reactive_power_offset_registers[phase], static_cast(q_offset)); + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); +} + +void ATM90E32Component::restore_gain_calibrations_() { + if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:"); + + for (uint8_t phase = 0; phase < 3; phase++) { + uint16_t v_gain = this->gain_phase_[phase].voltage_gain; + uint16_t i_gain = this->gain_phase_[phase].current_gain; + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain); + } + + this->write_gains_to_registers_(); + + if (this->verify_gain_writes_()) { + this->using_saved_calibrations_ = true; + ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully."); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly."); + } + } else { + this->using_saved_calibrations_ = false; + ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values."); + } +} + +void ATM90E32Component::restore_offset_calibrations_() { + if (this->offset_pref_.load(&this->offset_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); + + for (uint8_t phase = 0; phase < 3; phase++) { + auto &offset = this->offset_phase_[phase]; + write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_); + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase, + offset.voltage_offset_, offset.current_offset_); + } + } else { + ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); + } +} + +void ATM90E32Component::restore_power_offset_calibrations_() { + if (this->power_offset_pref_.load(&this->power_offset_phase_)) { + ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory."); + + for (uint8_t phase = 0; phase < 3; ++phase) { + auto &offset = this->power_offset_phase_[phase]; + write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset); + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, + offset.active_power_offset, offset.reactive_power_offset); + } + } else { + ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); + } +} + +void ATM90E32Component::clear_gain_calibrations() { + ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values..."); + + for (int phase = 0; phase < 3; phase++) { + gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_; + gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_; + } + + bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + this->using_saved_calibrations_ = false; + + if (success) { + ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:"); + for (int phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, + gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain); + } + } else { + ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!"); + } + + this->write_gains_to_registers_(); // Apply them to the chip immediately +} + +void ATM90E32Component::clear_offset_calibrations() { + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_offsets_to_registers_(phase, 0, 0); + } + + this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory + + ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); +} + +void ATM90E32Component::clear_power_offset_calibrations() { + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_power_offsets_to_registers_(phase, 0, 0); + } + + this->power_offset_pref_.save(&this->power_offset_phase_); + + ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); +} + +int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { const uint8_t num_reads = 5; uint64_t total_value = 0; - for (int i = 0; i < num_reads; ++i) { - const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); - total_value += measurement_value; + + for (uint8_t i = 0; i < num_reads; ++i) { + uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase) + : this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); + total_value += reading; } + const uint32_t average_value = total_value / num_reads; - const uint32_t current_offset = ~average_value + 1; - return current_offset & 0xFFFF; // Take the lower 16 bits + const uint32_t shifted = average_value >> 7; + const uint32_t offset = ~shifted + 1; + return static_cast(offset); // Takes lower 16 bits +} + +int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { + const uint8_t num_reads = 5; + uint64_t total_value = 0; + + for (uint8_t i = 0; i < num_reads; ++i) { + uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) + : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); + total_value += reading; + } + + const uint32_t average_value = total_value / num_reads; + const uint32_t power_offset = ~average_value + 1; + return static_cast(power_offset); // Takes the lower 16 bits +} + +bool ATM90E32Component::verify_gain_writes_() { + bool success = true; + for (uint8_t phase = 0; phase < 3; phase++) { + uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); + uint16_t read_current = this->read16_(current_gain_registers[phase]); + + if (read_voltage != this->gain_phase_[phase].voltage_gain || + read_current != this->gain_phase_[phase].current_gain) { + ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]); + success = false; + } + } + return success; // Return true if all writes were successful, false otherwise +} + +#ifdef USE_TEXT_SENSOR +void ATM90E32Component::check_phase_status() { + uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0); + uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1); + + for (int phase = 0; phase < 3; phase++) { + std::string status; + + if (state0 & over_voltage_flags[phase]) + status += "Over Voltage; "; + if (state1 & voltage_sag_flags[phase]) + status += "Voltage Sag; "; + if (state1 & phase_loss_flags[phase]) + status += "Phase Loss; "; + + auto *sensor = this->phase_status_text_sensor_[phase]; + const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; + if (!status.empty()) { + status.pop_back(); // remove space + status.pop_back(); // remove semicolon + ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str()); + if (sensor != nullptr) + sensor->publish_state(status); + } else { + if (sensor != nullptr) + sensor->publish_state("Okay"); + } + } +} + +void ATM90E32Component::check_freq_status() { + uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1); + + std::string freq_status; + + if (state1 & ATM90E32_STATUS_S1_FREQHIST) { + freq_status = "HIGH"; + } else if (state1 & ATM90E32_STATUS_S1_FREQLOST) { + freq_status = "LOW"; + } else { + freq_status = "Normal"; + } + ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); + + if (this->freq_status_text_sensor_ != nullptr) { + this->freq_status_text_sensor_->publish_state(freq_status); + } +} + +void ATM90E32Component::check_over_current() { + constexpr float max_current_threshold = 65.53f; + + for (uint8_t phase = 0; phase < 3; phase++) { + float current_val = + this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f; + + if (current_val > max_current_threshold) { + ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val); + ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2"); + if (this->phase_status_text_sensor_[phase] != nullptr) { + this->phase_status_text_sensor_[phase]->publish_state("Over Current; "); + } + } + } +} +#endif + +uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) { + // this assumes that 60Hz electrical systems use 120V mains, + // which is usually, but not always the case + float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; + float target_voltage = nominal_voltage * multiplier; + + float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V + float divider = (2.0f * ugain) / 32768.0f; + + float threshold = peak_01v / divider; + + return static_cast(threshold); +} + +bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) { + uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA); + if (last != expected) { + if (context != nullptr) { + ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last); + } else { + ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last); + } + return false; + } + return true; } } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 35c61d1e05..0703c40ae0 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -1,5 +1,6 @@ #pragma once +#include #include "atm90e32_reg.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" @@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent, static const uint8_t PHASEA = 0; static const uint8_t PHASEB = 1; static const uint8_t PHASEC = 2; + const char *phase_labels[3] = {"A", "B", "C"}; + // these registers are not sucessive, so we can't just do 'base + phase' + const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB, + ATM90E32_REGISTER_UGAINC}; + const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB, + ATM90E32_REGISTER_IGAINC}; + const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB, + ATM90E32_REGISTER_UOFFSETC}; + const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB, + ATM90E32_REGISTER_IOFFSETC}; + const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB, + ATM90E32_REGISTER_POFFSETC}; + const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB, + ATM90E32_REGISTER_QOFFSETC}; + const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST, + ATM90E32_STATUS_S0_OVPHASECST}; + const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST, + ATM90E32_STATUS_S1_SAGPHASECST}; + const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST, + ATM90E32_STATUS_S1_PHASELOSSCST}; void loop() override; void setup() override; void dump_config() override; @@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent, void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } + void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; } + void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; } + void set_active_power_offset(uint8_t phase, int16_t offset) { + this->power_offset_phase_[phase].active_power_offset = offset; + } + void set_reactive_power_offset(uint8_t phase, int16_t offset) { + this->power_offset_phase_[phase].reactive_power_offset = offset; + } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { @@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent, void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } void run_offset_calibrations(); + void run_power_offset_calibrations(); void clear_offset_calibrations(); + void clear_power_offset_calibrations(); + void clear_gain_calibrations(); void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } - uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); - uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); + void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; } + int16_t calibrate_offset(uint8_t phase, bool voltage); + int16_t calibrate_power_offset(uint8_t phase, bool reactive); + void run_gain_calibrations(); +#ifdef USE_NUMBER + void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; } + void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; } +#endif + float get_reference_voltage(uint8_t phase) { +#ifdef USE_NUMBER + return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage +#else + return 120.0; // Default voltage +#endif + } + float get_reference_current(uint8_t phase) { +#ifdef USE_NUMBER + return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current +#else + return 5.0f; // Default current +#endif + } + bool using_saved_calibrations_ = false; // Track if stored calibrations are being used +#ifdef USE_TEXT_SENSOR + void check_phase_status(); + void check_freq_status(); + void check_over_current(); + void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) { + this->phase_status_text_sensor_[phase] = sensor; + } + void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; } +#endif + uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier); int32_t last_periodic_millis = millis(); protected: +#ifdef USE_NUMBER + number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr}; + number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; +#endif uint16_t read16_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); void write16_(uint16_t a_register, uint16_t val); - float get_local_phase_voltage_(uint8_t /*phase*/); - float get_local_phase_current_(uint8_t /*phase*/); - float get_local_phase_active_power_(uint8_t /*phase*/); - float get_local_phase_reactive_power_(uint8_t /*phase*/); - float get_local_phase_power_factor_(uint8_t /*phase*/); - float get_local_phase_forward_active_energy_(uint8_t /*phase*/); - float get_local_phase_reverse_active_energy_(uint8_t /*phase*/); - float get_local_phase_angle_(uint8_t /*phase*/); - float get_local_phase_harmonic_active_power_(uint8_t /*phase*/); - float get_local_phase_peak_current_(uint8_t /*phase*/); - float get_phase_voltage_(uint8_t /*phase*/); - float get_phase_voltage_avg_(uint8_t /*phase*/); - float get_phase_current_(uint8_t /*phase*/); - float get_phase_current_avg_(uint8_t /*phase*/); - float get_phase_active_power_(uint8_t /*phase*/); - float get_phase_reactive_power_(uint8_t /*phase*/); - float get_phase_power_factor_(uint8_t /*phase*/); - float get_phase_forward_active_energy_(uint8_t /*phase*/); - float get_phase_reverse_active_energy_(uint8_t /*phase*/); - float get_phase_angle_(uint8_t /*phase*/); - float get_phase_harmonic_active_power_(uint8_t /*phase*/); - float get_phase_peak_current_(uint8_t /*phase*/); + float get_local_phase_voltage_(uint8_t phase); + float get_local_phase_current_(uint8_t phase); + float get_local_phase_active_power_(uint8_t phase); + float get_local_phase_reactive_power_(uint8_t phase); + float get_local_phase_apparent_power_(uint8_t phase); + float get_local_phase_power_factor_(uint8_t phase); + float get_local_phase_forward_active_energy_(uint8_t phase); + float get_local_phase_reverse_active_energy_(uint8_t phase); + float get_local_phase_angle_(uint8_t phase); + float get_local_phase_harmonic_active_power_(uint8_t phase); + float get_local_phase_peak_current_(uint8_t phase); + float get_phase_voltage_(uint8_t phase); + float get_phase_voltage_avg_(uint8_t phase); + float get_phase_current_(uint8_t phase); + float get_phase_current_avg_(uint8_t phase); + float get_phase_active_power_(uint8_t phase); + float get_phase_reactive_power_(uint8_t phase); + float get_phase_apparent_power_(uint8_t phase); + float get_phase_power_factor_(uint8_t phase); + float get_phase_forward_active_energy_(uint8_t phase); + float get_phase_reverse_active_energy_(uint8_t phase); + float get_phase_angle_(uint8_t phase); + float get_phase_harmonic_active_power_(uint8_t phase); + float get_phase_peak_current_(uint8_t phase); float get_frequency_(); float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; - void restore_calibrations_(); + void restore_offset_calibrations_(); + void restore_power_offset_calibrations_(); + void restore_gain_calibrations_(); + void save_gain_calibration_to_memory_(); + void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); + void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); + void write_gains_to_registers_(); + bool verify_gain_writes_(); + bool validate_spi_read_(uint16_t expected, const char *context = nullptr); struct ATM90E32Phase { uint16_t voltage_gain_{0}; uint16_t ct_gain_{0}; - uint16_t voltage_offset_{0}; - uint16_t current_offset_{0}; + int16_t voltage_offset_{0}; + int16_t current_offset_{0}; + int16_t active_power_offset_{0}; + int16_t reactive_power_offset_{0}; float voltage_{0}; float current_{0}; float active_power_{0}; float reactive_power_{0}; + float apparent_power_{0}; float power_factor_{0}; float forward_active_energy_{0}; float reverse_active_energy_{0}; @@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; - struct Calibration { - uint16_t voltage_offset_{0}; - uint16_t current_offset_{0}; + struct OffsetCalibration { + int16_t voltage_offset_{0}; + int16_t current_offset_{0}; } offset_phase_[3]; - ESPPreferenceObject pref_; + struct PowerOffsetCalibration { + int16_t active_power_offset{0}; + int16_t reactive_power_offset{0}; + } power_offset_phase_[3]; + + struct GainCalibration { + uint16_t voltage_gain{1}; + uint16_t current_gain{1}; + } gain_phase_[3]; + + ESPPreferenceObject offset_pref_; + ESPPreferenceObject power_offset_pref_; + ESPPreferenceObject gain_calibration_pref_; sensor::Sensor *freq_sensor_{nullptr}; +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr}; + text_sensor::TextSensor *freq_status_text_sensor_{nullptr}; +#endif sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; @@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent, bool publish_interval_flag_{false}; bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; + bool enable_gain_calibration_{false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index 954fb42e79..86c2d64569 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E /* POWER & P.F. REGISTERS */ static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) -static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P) +static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Active Power Reg Base (P) static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) -static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q) +static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Reactive Power Reg Base (Q) static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S) +static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9; // Apparent Mean Power Base (S) static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) @@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A Rea static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power) +static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9; // Lower Word Reg Base (Apparent Power) static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power) static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power) static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power) diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py index 931346b386..19f62ccfbd 100644 --- a/esphome/components/atm90e32/button/__init__.py +++ b/esphome/components/atm90e32/button/__init__.py @@ -1,43 +1,95 @@ import esphome.codegen as cg from esphome.components import button import esphome.config_validation as cv -from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE +from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE from .. import atm90e32_ns from ..sensor import ATM90E32Component +CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration" +CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration" CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" +CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration" +CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration" -ATM90E32CalibrationButton = atm90e32_ns.class_( - "ATM90E32CalibrationButton", - button.Button, +ATM90E32GainCalibrationButton = atm90e32_ns.class_( + "ATM90E32GainCalibrationButton", button.Button ) -ATM90E32ClearCalibrationButton = atm90e32_ns.class_( - "ATM90E32ClearCalibrationButton", - button.Button, +ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearGainCalibrationButton", button.Button +) +ATM90E32OffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32OffsetCalibrationButton", button.Button +) +ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearOffsetCalibrationButton", button.Button +) +ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32PowerOffsetCalibrationButton", button.Button +) +ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearPowerOffsetCalibrationButton", button.Button ) CONFIG_SCHEMA = { cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema( + ATM90E32GainCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:scale-balance", + ), + cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema( + ATM90E32ClearGainCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:delete", + ), cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( - ATM90E32CalibrationButton, + ATM90E32OffsetCalibrationButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_SCALE, ), cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( - ATM90E32ClearCalibrationButton, + ATM90E32ClearOffsetCalibrationButton, entity_category=ENTITY_CATEGORY_CONFIG, - icon=ICON_CHIP, + icon="mdi:delete", + ), + cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema( + ATM90E32PowerOffsetCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema( + ATM90E32ClearPowerOffsetCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:delete", ), } async def to_code(config): parent = await cg.get_variable(config[CONF_ID]) + + if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION): + b = await button.new_button(run_gain) + await cg.register_parented(b, parent) + + if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION): + b = await button.new_button(clear_gain) + await cg.register_parented(b, parent) + if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): b = await button.new_button(run_offset) await cg.register_parented(b, parent) + if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): b = await button.new_button(clear_offset) await cg.register_parented(b, parent) + + if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION): + b = await button.new_button(run_power) + await cg.register_parented(b, parent) + + if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION): + b = await button.new_button(clear_power) + await cg.register_parented(b, parent) diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp index 00715b61dd..a89f071997 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.cpp +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -1,4 +1,5 @@ #include "atm90e32_button.h" +#include "esphome/core/component.h" #include "esphome/core/log.h" namespace esphome { @@ -6,15 +7,73 @@ namespace atm90e32 { static const char *const TAG = "atm90e32.button"; -void ATM90E32CalibrationButton::press_action() { - ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); +void ATM90E32GainCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, + "[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values"); + this->parent_->run_gain_calibrations(); +} + +void ATM90E32ClearGainCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + this->parent_->clear_gain_calibrations(); +} + +void ATM90E32OffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**"); + ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save " + "these values"); this->parent_->run_offset_calibrations(); } -void ATM90E32ClearCalibrationButton::press_action() { - ESP_LOGI(TAG, "Offset calibrations cleared."); +void ATM90E32ClearOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); this->parent_->clear_offset_calibrations(); } +void ATM90E32PowerOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**"); + ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config " + "file to save these values"); + this->parent_->run_power_offset_calibrations(); +} + +void ATM90E32ClearPowerOffsetCalibrationButton::press_action() { + if (this->parent_ == nullptr) { + ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str()); + return; + } + + ESP_LOGI(TAG, "%s", this->get_name().c_str()); + this->parent_->clear_power_offset_calibrations(); +} + } // namespace atm90e32 } // namespace esphome diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h index 0617099457..2449581531 100644 --- a/esphome/components/atm90e32/button/atm90e32_button.h +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -7,17 +7,49 @@ namespace esphome { namespace atm90e32 { -class ATM90E32CalibrationButton : public button::Button, public Parented { +class ATM90E32GainCalibrationButton : public button::Button, public Parented { public: - ATM90E32CalibrationButton() = default; + ATM90E32GainCalibrationButton() = default; protected: void press_action() override; }; -class ATM90E32ClearCalibrationButton : public button::Button, public Parented { +class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented { public: - ATM90E32ClearCalibrationButton() = default; + ATM90E32ClearGainCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32OffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32OffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearOffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32PowerOffsetCalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearPowerOffsetCalibrationButton() = default; protected: void press_action() override; diff --git a/esphome/components/atm90e32/number/__init__.py b/esphome/components/atm90e32/number/__init__.py new file mode 100644 index 0000000000..848680b875 --- /dev/null +++ b/esphome/components/atm90e32/number/__init__.py @@ -0,0 +1,130 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_PHASE_A, + CONF_PHASE_B, + CONF_PHASE_C, + CONF_REFERENCE_VOLTAGE, + CONF_STEP, + ENTITY_CATEGORY_CONFIG, + UNIT_AMPERE, + UNIT_VOLT, +) + +from .. import atm90e32_ns +from ..sensor import ATM90E32Component + +ATM90E32Number = atm90e32_ns.class_( + "ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component) +) + +CONF_REFERENCE_CURRENT = "reference_current" +PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C] + + +REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE, default="box"): cv.string, + cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_, + cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_, + cv.Optional(CONF_STEP, default=0.1): cv.float_, + } + ).extend( + number.number_schema( + class_=ATM90E32Number, + unit_of_measurement=UNIT_VOLT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:power-plug", + ) + ) +) + + +REFERENCE_CURRENT_PHASE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE, default="box"): cv.string, + cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_, + cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_, + cv.Optional(CONF_STEP, default=0.1): cv.float_, + } + ).extend( + number.number_schema( + class_=ATM90E32Number, + unit_of_measurement=UNIT_AMPERE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon="mdi:home-lightning-bolt", + ) + ) +) + + +REFERENCE_VOLTAGE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA, + } +) + +REFERENCE_CURRENT_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA, + cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA, + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE): + voltage_objs = [None, None, None] + + for i, key in enumerate(PHASE_KEYS): + if validated := voltage_cfg.get(key): + obj = await number.new_number( + validated, + min_value=validated["min_value"], + max_value=validated["max_value"], + step=validated["step"], + ) + await cg.register_parented(obj, parent) + voltage_objs[i] = obj + + # Inherit from A → B/C if only A defined + if voltage_objs[0] is not None: + for i in range(3): + if voltage_objs[i] is None: + voltage_objs[i] = voltage_objs[0] + + for i, obj in enumerate(voltage_objs): + if obj is not None: + cg.add(parent.set_reference_voltage(i, obj)) + + if current_cfg := config.get(CONF_REFERENCE_CURRENT): + for i, key in enumerate(PHASE_KEYS): + if validated := current_cfg.get(key): + obj = await number.new_number( + validated, + min_value=validated["min_value"], + max_value=validated["max_value"], + step=validated["step"], + ) + await cg.register_parented(obj, parent) + cg.add(parent.set_reference_current(i, obj)) diff --git a/esphome/components/atm90e32/number/atm90e32_number.h b/esphome/components/atm90e32/number/atm90e32_number.h new file mode 100644 index 0000000000..9b6129b26d --- /dev/null +++ b/esphome/components/atm90e32/number/atm90e32_number.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/atm90e32/atm90e32.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32Number : public number::Number, public Parented { + public: + void control(float value) override { this->publish_state(value); } +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 0dc3bfdc4f..7cdbd69f56 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -33,6 +33,7 @@ from esphome.const import ( UNIT_DEGREES, UNIT_HERTZ, UNIT_VOLT, + UNIT_VOLT_AMPS, UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT, UNIT_WATT_HOURS, @@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga" CONF_CURRENT_PHASES = "current_phases" CONF_GAIN_VOLTAGE = "gain_voltage" CONF_GAIN_CT = "gain_ct" +CONF_OFFSET_VOLTAGE = "offset_voltage" +CONF_OFFSET_CURRENT = "offset_current" +CONF_OFFSET_ACTIVE_POWER = "offset_active_power" +CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power" CONF_HARMONIC_POWER = "harmonic_power" CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" +CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration" +CONF_PHASE_STATUS = "phase_status" +CONF_FREQUENCY_STATUS = "frequency_status" UNIT_DEG = "degrees" LINE_FREQS = { "50HZ": 50, @@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, + unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, @@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, + cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_, + cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_, + cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_, } ) @@ -164,9 +177,10 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( CURRENT_PHASES, upper=True ), - cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), + cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -185,6 +199,10 @@ async def to_code(config): conf = config[phase] cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) + cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE])) + cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT])) + cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER])) + cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER])) if voltage_config := conf.get(CONF_VOLTAGE): sens = await sensor.new_sensor(voltage_config) cg.add(var.set_voltage_sensor(i, sens)) @@ -218,16 +236,15 @@ async def to_code(config): if peak_current_config := conf.get(CONF_PEAK_CURRENT): sens = await sensor.new_sensor(peak_current_config) cg.add(var.set_peak_current_sensor(i, sens)) - if frequency_config := config.get(CONF_FREQUENCY): sens = await sensor.new_sensor(frequency_config) cg.add(var.set_freq_sensor(sens)) if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE): sens = await sensor.new_sensor(chip_temperature_config) cg.add(var.set_chip_temperature_sensor(sens)) - cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) + cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION])) diff --git a/esphome/components/atm90e32/text_sensor/__init__.py b/esphome/components/atm90e32/text_sensor/__init__.py new file mode 100644 index 0000000000..ab96f6c207 --- /dev/null +++ b/esphome/components/atm90e32/text_sensor/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C + +from ..sensor import ATM90E32Component + +CONF_PHASE_STATUS = "phase_status" +CONF_FREQUENCY_STATUS = "frequency_status" +PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C] + +PHASE_STATUS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema( + icon="mdi:flash-alert" + ), + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(ATM90E32Component), + cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA, + cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema( + icon="mdi:lightbulb-alert" + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if phase_cfg := config.get(CONF_PHASE_STATUS): + for i, key in enumerate(PHASE_KEYS): + if sub_phase_cfg := phase_cfg.get(key): + sens = await text_sensor.new_text_sensor(sub_phase_cfg) + cg.add(parent.set_phase_status_text_sensor(i, sens)) + + if freq_status_config := config.get(CONF_FREQUENCY_STATUS): + sens = await text_sensor.new_text_sensor(freq_status_config) + cg.add(parent.set_freq_status_text_sensor(sens)) diff --git a/tests/components/atm90e32/common.yaml b/tests/components/atm90e32/common.yaml index 156d00b4e0..3eeed8395f 100644 --- a/tests/components/atm90e32/common.yaml +++ b/tests/components/atm90e32/common.yaml @@ -17,10 +17,22 @@ sensor: name: EMON Active Power CT1 reactive_power: name: EMON Reactive Power CT1 + apparent_power: + name: EMON Apparent Power CT1 + harmonic_power: + name: EMON Harmonic Power CT1 power_factor: name: EMON Power Factor CT1 + phase_angle: + name: EMON Phase Angle CT1 + peak_current: + name: EMON Peak Current CT1 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 phase_b: current: name: EMON CT2 Current @@ -28,10 +40,22 @@ sensor: name: EMON Active Power CT2 reactive_power: name: EMON Reactive Power CT2 + apparent_power: + name: EMON Apparent Power CT2 + harmonic_power: + name: EMON Harmonic Power CT2 power_factor: name: EMON Power Factor CT2 + phase_angle: + name: EMON Phase Angle CT2 + peak_current: + name: EMON Peak Current CT2 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 phase_c: current: name: EMON CT3 Current @@ -39,23 +63,75 @@ sensor: name: EMON Active Power CT3 reactive_power: name: EMON Reactive Power CT3 + apparent_power: + name: EMON Apparent Power CT3 + harmonic_power: + name: EMON Harmonic Power CT3 power_factor: name: EMON Power Factor CT3 + phase_angle: + name: EMON Phase Angle CT3 + peak_current: + name: EMON Peak Current CT3 gain_voltage: 7305 gain_ct: 27961 + offset_voltage: 0 + offset_current: 0 + offset_active_power: 0 + offset_reactive_power: 0 frequency: name: EMON Line Frequency chip_temperature: - name: EMON Chip Temp A + name: EMON Chip Temp line_frequency: 60Hz current_phases: 3 - gain_pga: 2X + gain_pga: 1X enable_offset_calibration: True + enable_gain_calibration: True + +text_sensor: + - platform: atm90e32 + id: atm90e32_chip1 + phase_status: + phase_a: + name: "Phase A Status" + phase_b: + name: "Phase B Status" + phase_c: + name: "Phase C Status" + frequency_status: + name: "Frequency Status" button: - platform: atm90e32 id: atm90e32_chip1 + run_gain_calibration: + name: "Run Gain Calibration" + clear_gain_calibration: + name: "Clear Gain Calibration" run_offset_calibration: - name: Chip1 - Run Offset Calibration + name: "Run Offset Calibration" clear_offset_calibration: - name: Chip1 - Clear Offset Calibration + name: "Clear Offset Calibration" + run_power_offset_calibration: + name: "Run Power Offset Calibration" + clear_power_offset_calibration: + name: "Clear Power Offset Calibration" + +number: + - platform: atm90e32 + id: atm90e32_chip1 + reference_voltage: + phase_a: + name: "Phase A Ref Voltage" + phase_b: + name: "Phase B Ref Voltage" + phase_c: + name: "Phase C Ref Voltage" + reference_current: + phase_a: + name: "Phase A Ref Current" + phase_b: + name: "Phase B Ref Current" + phase_c: + name: "Phase C Ref Current"