diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 36086579fc..63f059eb6d 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,7 +22,7 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Generate a token id: generate-token diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index f51bd84186..c7cc720323 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 1c7a62e40b..c7da7f6672 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index d6dac66359..61ecf8183b 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,7 +43,7 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e503bf556e..e50f2aef40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -136,7 +136,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -179,7 +179,7 @@ jobs: component-test-count: ${{ steps.determine.outputs.component-test-count }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -214,7 +214,7 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python 3.13 id: python uses: actions/setup-python@v5.6.0 @@ -287,7 +287,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -374,7 +374,7 @@ jobs: sudo apt-get install libsdl2-dev - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -400,7 +400,7 @@ jobs: matrix: ${{ steps.split.outputs.components }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Split components into 20 groups id: split run: | @@ -430,7 +430,7 @@ jobs: sudo apt-get install libsdl2-dev - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -459,7 +459,7 @@ jobs: if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ddeb0a99d2..7a7c39aeec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82cf605342..9af67aa310 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: @@ -92,7 +92,7 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Download digests uses: actions/download-artifact@v5.0.0 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index a38825fc45..cc03ed3e3f 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Checkout Home Assistant - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: repository: home-assistant/core path: lib/home-assistant diff --git a/CODEOWNERS b/CODEOWNERS index 509b0c0b9e..a1a74e9c99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ esphome/components/kuntze/* @ssieb esphome/components/lc709203f/* @ilikecake esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2412/* @Rihan9 esphome/components/ld2420/* @descipher esphome/components/ld2450/* @hareeshmu esphome/components/ld24xx/* @kbx81 diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index 1a437a68a2..acb1c4d9db 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_DIRECTION, CONF_HYSTERESIS, CONF_ID, + CONF_POWER_MODE, CONF_RANGE, ) @@ -57,7 +58,6 @@ FAST_FILTER = { CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_WATCHDOG = "watchdog" -CONF_POWER_MODE = "power_mode" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_START_POSITION = "start_position" diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index cfc38d796d..1491852e07 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -24,7 +24,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_WATCHDOG = "watchdog" -CONF_POWER_MODE = "power_mode" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_PWM_FREQUENCY = "pwm_frequency" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index a887e7a9e6..d5fee6ea04 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -110,6 +110,8 @@ void ATM90E32Component::update() { void ATM90E32Component::setup() { this->spi_setup(); + this->cs_summary_ = this->cs_->dump_summary(); + const char *cs = this->cs_summary_.c_str(); uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t high_thresh = 0; @@ -130,9 +132,9 @@ void ATM90E32Component::setup() { mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) } - 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 + this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false); // Perform soft reset + delay(6); // Wait for the minimum 5ms + 1ms + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access if (!this->validate_spi_read_(0x55AA, "setup()")) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); @@ -156,16 +158,17 @@ void ATM90E32Component::setup() { 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()); + uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_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()); + uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_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."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(this->voltage_offset_registers[phase], static_cast(this->offset_phase_[phase].voltage_offset_)); @@ -180,21 +183,18 @@ void ATM90E32Component::setup() { 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()); + uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_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 { + if (!this->using_saved_calibrations_) { 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."); - + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); 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_); @@ -213,6 +213,122 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } +void ATM90E32Component::log_calibration_status_() { + const char *cs = this->cs_summary_.c_str(); + + bool offset_mismatch = false; + bool power_mismatch = false; + bool gain_mismatch = false; + + for (uint8_t phase = 0; phase < 3; ++phase) { + offset_mismatch |= this->offset_calibration_mismatch_[phase]; + power_mismatch |= this->power_offset_calibration_mismatch_[phase]; + gain_mismatch |= this->gain_calibration_mismatch_[phase]; + } + + if (offset_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_, + this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (power_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].active_power_offset, + this->config_power_offset_phase_[phase].reactive_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (gain_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6u | %6u | %6u | %6u |", cs, 'A' + phase, + this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain, + this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (!this->enable_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); + } else if (this->restored_offset_calibration_ && !offset_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); + } + + if (this->restored_power_offset_calibration_ && !power_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + } + if (!this->enable_gain_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); + } else if (this->restored_gain_calibration_ && !gain_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs); + } + this->calibration_message_printed_ = true; +} + void ATM90E32Component::dump_config() { ESP_LOGCONFIG("", "ATM90E32:"); LOG_PIN(" CS Pin: ", this->cs_); @@ -255,6 +371,10 @@ void ATM90E32Component::dump_config() { 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_); + if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ || + this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) { + this->log_calibration_status_(); + } } float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; } @@ -262,26 +382,35 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; // R/C registers can conly be cleared after the LastSPIData register is updated (register 78H) // Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period // Default is 143FH (20ms, 63ms) -uint16_t ATM90E32Component::read16_(uint16_t a_register) { +uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) { uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrl = (a_register & 0xFF); - uint8_t data[2]; - uint16_t output; - this->enable(); - 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); - this->disable(); - - output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + uint8_t data[4] = {addrh, addrl, 0x00, 0x00}; + this->transfer_array(data, 4); + uint16_t output = encode_uint16(data[2], data[3]); ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); return output; } +uint16_t ATM90E32Component::read16_(uint16_t a_register) { + this->enable(); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty + uint16_t output = this->read16_transaction_(a_register); + delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS + this->disable(); + delay_microseconds_safe(1); // meet minimum CS high time before next transaction + return output; +} + int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { - const uint16_t val_h = this->read16_(addr_h); - const uint16_t val_l = this->read16_(addr_l); + this->enable(); + delay_microseconds_safe(1); + const uint16_t val_h = this->read16_transaction_(addr_h); + delay_microseconds_safe(1); + const uint16_t val_l = this->read16_transaction_(addr_l); + delay_microseconds_safe(1); + this->disable(); + delay_microseconds_safe(1); const int32_t val = (val_h << 16) | val_l; ESP_LOGVV(TAG, @@ -292,13 +421,19 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { return val; } -void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { +void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) { ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val); + uint8_t addrh = ((a_register >> 8) & 0x03); + uint8_t addrl = (a_register & 0xFF); + uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)}; this->enable(); - this->write_byte16(a_register); - this->write_byte16(val); + delay_microseconds_safe(1); // ensure CS setup time + this->write_array(data, 4); + delay_microseconds_safe(1); // allow clock to settle before raising CS this->disable(); - this->validate_spi_read_(val, "write16()"); + delay_microseconds_safe(1); // ensure minimum CS high time + if (validate) + this->validate_spi_read_(val, "write16()"); } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } @@ -441,8 +576,10 @@ float ATM90E32Component::get_chip_temperature_() { } void ATM90E32Component::run_gain_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_gain_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); + ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true", + cs); return; } @@ -454,12 +591,14 @@ void ATM90E32Component::run_gain_calibrations() { 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] ---------------------------------------------------------------------"); + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration =========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI( + TAG, + "[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |", + cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { float measured_voltage = this->get_phase_voltage_avg_(phase); @@ -476,22 +615,22 @@ void ATM90E32Component::run_gain_calibrations() { // Voltage calibration if (ref_voltage <= 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs, phase_labels[phase]); } else if (measured_voltage == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, 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.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, 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]); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage " + "transformer.", + cs, phase_labels[phase]); new_voltage_gain = 65535; } this->gain_phase_[phase].voltage_gain = static_cast(new_voltage_gain); @@ -501,20 +640,20 @@ void ATM90E32Component::run_gain_calibrations() { // Current calibration if (ref_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs, phase_labels[phase]); } else if (measured_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, 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.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, 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]); + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", + cs, phase_labels[phase]); new_current_gain = 65535; } this->gain_phase_[phase].current_gain = static_cast(new_current_gain); @@ -523,13 +662,13 @@ void ATM90E32Component::run_gain_calibrations() { } // Final row output - ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", cs, '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"); + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); this->save_gain_calibration_to_memory_(); this->write_gains_to_registers_(); @@ -537,54 +676,108 @@ void ATM90E32Component::run_gain_calibrations() { } void ATM90E32Component::save_gain_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + global_preferences->sync(); if (success) { this->using_saved_calibrations_ = true; - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs); } else { this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs); + } +} + +void ATM90E32Component::save_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->offset_pref_.save(&this->offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_offset_calibration_ = true; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs); + } +} + +void ATM90E32Component::save_power_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->power_offset_pref_.save(&this->power_offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_power_offset_calibration_ = true; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs); } } void ATM90E32Component::run_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + 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, + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); } - this->offset_pref_.save(&this->offset_phase_); // Save to flash + ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); + + this->save_offset_calibration_to_memory_(); } void ATM90E32Component::run_power_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { ESP_LOGW( TAG, - "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); + "[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + 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); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash + this->save_power_offset_calibration_to_memory_(); } void ATM90E32Component::write_gains_to_registers_() { @@ -631,102 +824,276 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t } 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."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) { + this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_; + this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_; + this->gain_phase_[i] = this->config_gain_phase_[i]; } + + if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + bool all_zero = true; + bool same_as_config = true; + for (uint8_t phase = 0; phase < 3; ++phase) { + const auto &cfg = this->config_gain_phase_[phase]; + const auto &saved = this->gain_phase_[phase]; + if (saved.voltage_gain != 0 || saved.current_gain != 0) + all_zero = false; + if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain) + same_as_config = false; + } + + if (!all_zero && !same_as_config) { + for (uint8_t phase = 0; phase < 3; ++phase) { + bool mismatch = false; + if (this->has_config_voltage_gain_[phase] && + this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain) + mismatch = true; + if (this->has_config_current_gain_[phase] && + this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain) + mismatch = true; + if (mismatch) + this->gain_calibration_mismatch_[phase] = true; + } + + this->write_gains_to_registers_(); + + if (this->verify_gain_writes_()) { + this->using_saved_calibrations_ = true; + this->restored_gain_calibration_ = true; + return; + } + + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs); + } + } + + this->using_saved_calibrations_ = false; + for (uint8_t i = 0; i < 3; ++i) + this->gain_phase_[i] = this->config_gain_phase_[i]; + this->write_gains_to_registers_(); + + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); } void ATM90E32Component::restore_offset_calibrations_() { - if (this->offset_pref_.load(&this->offset_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_offset_phase_[i] = this->offset_phase_[i]; + bool have_data = this->offset_pref_.load(&this->offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->offset_phase_) { + if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_offset_calibration_ = true; 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_); + bool mismatch = false; + if (this->has_config_voltage_offset_[phase] && + offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_) + mismatch = true; + if (this->has_config_current_offset_[phase] && + offset.current_offset_ != this->config_offset_phase_[phase].current_offset_) + mismatch = true; + if (mismatch) + this->offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); + for (uint8_t phase = 0; phase < 3; phase++) + this->offset_phase_[phase] = this->config_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; phase++) { + write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_, + this->offset_phase_[phase].current_offset_); } } 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."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; + bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->power_offset_phase_) { + if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_power_offset_calibration_ = true; 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); + bool mismatch = false; + if (this->has_config_active_power_offset_[phase] && + offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset) + mismatch = true; + if (this->has_config_reactive_power_offset_[phase] && + offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset) + mismatch = true; + if (mismatch) + this->power_offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); + for (uint8_t phase = 0; phase < 3; ++phase) + this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; ++phase) { + write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); } } 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_; + const char *cs = this->cs_summary_.c_str(); + if (!this->using_saved_calibrations_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + for (int phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + return; } - bool success = this->gain_calibration_pref_.save(&this->gain_phase_); - this->using_saved_calibrations_ = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); - 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!"); + for (int phase = 0; phase < 3; phase++) { + uint16_t voltage_gain = this->phase_[phase].voltage_gain_; + uint16_t current_gain = this->phase_[phase].ct_gain_; + + this->config_gain_phase_[phase].voltage_gain = voltage_gain; + this->config_gain_phase_[phase].current_gain = current_gain; + this->gain_phase_[phase].voltage_gain = voltage_gain; + this->gain_phase_[phase].current_gain = current_gain; + + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, voltage_gain, current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + + GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}}; + bool success = this->gain_calibration_pref_.save(&zero_gains); + global_preferences->sync(); + + this->using_saved_calibrations_ = false; + this->restored_gain_calibration_ = false; + for (bool &phase : this->gain_calibration_mismatch_) + phase = false; + + if (!success) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs); } 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); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + return; } - this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t voltage_offset = + this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0; + int16_t current_offset = + this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0; + this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, + current_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + + OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->offset_pref_.save(&zero_offsets); // Clear stored values in flash + global_preferences->sync(); + + this->restored_offset_calibration_ = false; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs); } void ATM90E32Component::clear_power_offset_calibrations() { - for (uint8_t phase = 0; phase < 3; phase++) { - this->write_power_offsets_to_registers_(phase, 0, 0); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_power_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + return; } - this->power_offset_pref_.save(&this->power_offset_phase_); + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t active_offset = + this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0; + int16_t reactive_offset = this->has_config_reactive_power_offset_[phase] + ? this->config_power_offset_phase_[phase].reactive_power_offset + : 0; + this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + + PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->power_offset_pref_.save(&zero_power_offsets); + global_preferences->sync(); + + this->restored_power_offset_calibration_ = false; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs); } int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { @@ -747,20 +1114,21 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { const uint8_t num_reads = 5; - uint64_t total_value = 0; + int64_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); + int32_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; + int32_t average_value = total_value / num_reads; + int32_t power_offset = -average_value; return static_cast(power_offset); // Takes the lower 16 bits } bool ATM90E32Component::verify_gain_writes_() { + const char *cs = this->cs_summary_.c_str(); bool success = true; for (uint8_t phase = 0; phase < 3; phase++) { uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); @@ -768,7 +1136,7 @@ bool ATM90E32Component::verify_gain_writes_() { 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]); + ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]); success = false; } } @@ -791,16 +1159,16 @@ void ATM90E32Component::check_phase_status() { status += "Phase Loss; "; auto *sensor = this->phase_status_text_sensor_[phase]; - const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; + if (sensor == nullptr) + continue; + 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); + ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str()); + sensor->publish_state(status); } else { - if (sensor != nullptr) - sensor->publish_state("Okay"); + sensor->publish_state("Okay"); } } } @@ -817,9 +1185,12 @@ void ATM90E32Component::check_freq_status() { } else { freq_status = "Normal"; } - ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); - if (this->freq_status_text_sensor_ != nullptr) { + if (freq_status == "Normal") { + ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str()); + } else { + ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); + } this->freq_status_text_sensor_->publish_state(freq_status); } } diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 0703c40ae0..afbd9cf941 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -61,15 +61,29 @@ class ATM90E32Component : public PollingComponent, this->phase_[phase].harmonic_active_power_sensor_ = obj; } 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_volt_gain(int phase, uint16_t gain) { + this->phase_[phase].voltage_gain_ = gain; + this->has_config_voltage_gain_[phase] = true; + } + void set_ct_gain(int phase, uint16_t gain) { + this->phase_[phase].ct_gain_ = gain; + this->has_config_current_gain_[phase] = true; + } + void set_voltage_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].voltage_offset_ = offset; + this->has_config_voltage_offset_[phase] = true; + } + void set_current_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].current_offset_ = offset; + this->has_config_current_offset_[phase] = true; + } void set_active_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].active_power_offset = offset; + this->has_config_active_power_offset_[phase] = true; } void set_reactive_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].reactive_power_offset = offset; + this->has_config_reactive_power_offset_[phase] = true; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } @@ -126,8 +140,9 @@ class ATM90E32Component : public PollingComponent, number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; #endif uint16_t read16_(uint16_t a_register); + uint16_t read16_transaction_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); - void write16_(uint16_t a_register, uint16_t val); + void write16_(uint16_t a_register, uint16_t val, bool validate = true); 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); @@ -159,12 +174,15 @@ class ATM90E32Component : public PollingComponent, void restore_offset_calibrations_(); void restore_power_offset_calibrations_(); void restore_gain_calibrations_(); + void save_offset_calibration_to_memory_(); void save_gain_calibration_to_memory_(); + void save_power_offset_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); + void log_calibration_status_(); struct ATM90E32Phase { uint16_t voltage_gain_{0}; @@ -204,19 +222,33 @@ class ATM90E32Component : public PollingComponent, int16_t current_offset_{0}; } offset_phase_[3]; + OffsetCalibration config_offset_phase_[3]; + struct PowerOffsetCalibration { int16_t active_power_offset{0}; int16_t reactive_power_offset{0}; } power_offset_phase_[3]; + PowerOffsetCalibration config_power_offset_phase_[3]; + struct GainCalibration { uint16_t voltage_gain{1}; uint16_t current_gain{1}; } gain_phase_[3]; + GainCalibration config_gain_phase_[3]; + + bool has_config_voltage_offset_[3]{false, false, false}; + bool has_config_current_offset_[3]{false, false, false}; + bool has_config_active_power_offset_[3]{false, false, false}; + bool has_config_reactive_power_offset_[3]{false, false, false}; + bool has_config_voltage_gain_[3]{false, false, false}; + bool has_config_current_gain_[3]{false, false, false}; + ESPPreferenceObject offset_pref_; ESPPreferenceObject power_offset_pref_; ESPPreferenceObject gain_calibration_pref_; + std::string cs_summary_; sensor::Sensor *freq_sensor_{nullptr}; #ifdef USE_TEXT_SENSOR @@ -231,6 +263,13 @@ class ATM90E32Component : public PollingComponent, bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; + bool restored_offset_calibration_{false}; + bool restored_power_offset_calibration_{false}; + bool restored_gain_calibration_{false}; + bool calibration_message_printed_{false}; + bool offset_calibration_mismatch_[3]{false, false, false}; + bool power_offset_calibration_mismatch_[3]{false, false, false}; + bool gain_calibration_mismatch_[3]{false, false, false}; }; } // namespace atm90e32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index b16b894188..6eb38d5b88 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -119,7 +119,7 @@ void BluetoothConnection::loop() { // Check if we should disable the loop // - For V3_WITH_CACHE: Services are never sent, disable after INIT state - // - For other connections: Disable only after service discovery is complete + // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->send_service_ == DONE_SENDING_SERVICES)) { @@ -146,10 +146,7 @@ void BluetoothConnection::send_service_for_discovery_() { if (this->send_service_ >= this->service_count_) { this->send_service_ = DONE_SENDING_SERVICES; this->proxy_->send_gatt_services_done(this->address_); - if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { - this->release_services(); - } + this->release_services(); return; } diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py new file mode 100644 index 0000000000..e701d0bda9 --- /dev/null +++ b/esphome/components/ld2412/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_THROTTLE + +AUTO_LOAD = ["ld24xx"] +CODEOWNERS = ["@Rihan9"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +LD2412_ns = cg.esphome_ns.namespace("ld2412") +LD2412Component = LD2412_ns.class_("LD2412Component", cg.Component, uart.UARTDevice) + +CONF_LD2412_ID = "ld2412_id" + +CONF_MAX_MOVE_DISTANCE = "max_move_distance" +CONF_MAX_STILL_DISTANCE = "max_still_distance" +CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2412Component), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2412", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2412/binary_sensor.py b/esphome/components/ld2412/binary_sensor.py new file mode 100644 index 0000000000..aa1b0d2cd8 --- /dev/null +++ b/esphome/components/ld2412/binary_sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HAS_MOVING_TARGET, + CONF_HAS_STILL_TARGET, + CONF_HAS_TARGET, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_RUNNING, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_MOTION_SENSOR, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS = "dynamic_background_correction_status" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if dynamic_background_correction_status_config := config.get( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): + sens = await binary_sensor.new_binary_sensor( + dynamic_background_correction_status_config + ) + cg.add( + LD2412_component.set_dynamic_background_correction_status_binary_sensor( + sens + ) + ) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(LD2412_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(LD2412_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(LD2412_component.set_still_target_binary_sensor(sens)) diff --git a/esphome/components/ld2412/button/__init__.py b/esphome/components/ld2412/button/__init__.py new file mode 100644 index 0000000000..e78cad4b88 --- /dev/null +++ b/esphome/components/ld2412/button/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_DATABASE, + ICON_PULSE, + ICON_RESTART, + ICON_RESTART_ALERT, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button) +QueryButton = LD2412_ns.class_("QueryButton", button.Button) +RestartButton = LD2412_ns.class_("RestartButton", button.Button) +StartDynamicBackgroundCorrectionButton = LD2412_ns.class_( + "StartDynamicBackgroundCorrectionButton", button.Button +) + +CONF_QUERY_PARAMS = "query_params" +CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + FactoryResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + QueryButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), + cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema( + StartDynamicBackgroundCorrectionButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_factory_reset_button(b)) + if query_params_config := config.get(CONF_QUERY_PARAMS): + b = await button.new_button(query_params_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_query_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_restart_button(b)) + if start_dynamic_background_correction_config := config.get( + CONF_START_DYNAMIC_BACKGROUND_CORRECTION + ): + b = await button.new_button(start_dynamic_background_correction_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_start_dynamic_background_correction_button(b)) diff --git a/esphome/components/ld2412/button/factory_reset_button.cpp b/esphome/components/ld2412/button/factory_reset_button.cpp new file mode 100644 index 0000000000..7ee85bc8f9 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.cpp @@ -0,0 +1,9 @@ +#include "factory_reset_button.h" + +namespace esphome { +namespace ld2412 { + +void FactoryResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h new file mode 100644 index 0000000000..36a3fffcd5 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class FactoryResetButton : public button::Button, public Parented { + public: + FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.cpp b/esphome/components/ld2412/button/query_button.cpp new file mode 100644 index 0000000000..536f74427f --- /dev/null +++ b/esphome/components/ld2412/button/query_button.cpp @@ -0,0 +1,9 @@ +#include "query_button.h" + +namespace esphome { +namespace ld2412 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h new file mode 100644 index 0000000000..595ef6d1e9 --- /dev/null +++ b/esphome/components/ld2412/button/query_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp new file mode 100644 index 0000000000..aca0d17841 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h new file mode 100644 index 0000000000..5cd582e2a3 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp new file mode 100644 index 0000000000..9b37243b82 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -0,0 +1,11 @@ +#include "start_dynamic_background_correction_button.h" + +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h new file mode 100644 index 0000000000..3af0a8a149 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { + public: + StartDynamicBackgroundCorrectionButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp new file mode 100644 index 0000000000..63af69ce0d --- /dev/null +++ b/esphome/components/ld2412/ld2412.cpp @@ -0,0 +1,861 @@ +#include "ld2412.h" + +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ld2412 { + +static const char *const TAG = "ld2412"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolution : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x03, + DISTANCE_RESOLUTION_0_5 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunction : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevel : uint8_t { + OUT_PIN_LEVEL_LOW = 0x01, + OUT_PIN_LEVEL_HIGH = 0x00, +}; + +/* +Data Type: 6th byte +Target states: 9th byte + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes +*/ +enum PeriodicData : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + MOVING_SENSOR_START = 17, + STILL_SENSOR_START = 31, + LIGHT_SENSOR = 45, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0XAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.5m", DISTANCE_RESOLUTION_0_5}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_5, "0.5m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) { + return entry.value; + } + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) { + return entry.str; + } + } + return ""; // Not found +} + +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined +// Commands +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_ENABLE_ENG = 0x62; +static constexpr uint8_t CMD_DISABLE_ENG = 0x63; +static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12; +static constexpr uint8_t CMD_BASIC_CONF = 0x02; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11; +static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01; +static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C; +static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_FACTORY_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B; +static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B; +static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03; +static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13; +static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04; +static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14; +static constexpr uint8_t CMD_NONE = 0x00; +// Commands values +static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; +static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; +static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; +// MAC address the module uses when Bluetooth is disabled +static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } + +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; +} + +void LD2412Component::dump_config() { + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGCONFIG(TAG, + "LD2412:\n" + " Firmware version: %s\n" + " MAC address: %s", + version.c_str(), mac_str.c_str()); +#ifdef USE_BINARY_SENSOR + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", + this->dynamic_background_correction_status_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); +#endif +#ifdef USE_SENSOR + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); + } + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); + } +#endif +#ifdef USE_TEXT_SENSOR + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); +#endif +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_); + LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_); + LOG_NUMBER(" ", "Timeout", this->timeout_number_); + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds", n); + } + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds", n); + } +#endif +#ifdef USE_SELECT + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_); + LOG_SELECT(" ", "LightFunction", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); +#endif +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); + LOG_BUTTON(" ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_); +#endif +} + +void LD2412Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + this->read_all_info(); +} + +void LD2412Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + delay(10); // NOLINT + this->get_mac_(); + delay(10); // NOLINT + this->get_distance_resolution_(); + delay(10); // NOLINT + this->query_parameters_(); + delay(10); // NOLINT + this->query_dynamic_background_correction_(); + delay(10); // NOLINT + this->query_light_control_(); + delay(10); // NOLINT +#ifdef USE_NUMBER + this->get_gate_threshold(); + delay(10); // NOLINT +#endif + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2412Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + +void LD2412Component::loop() { + while (this->available()) { + this->readline_(this->read()); + } +} + +void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE); + // length bytes + uint8_t len = 2; + if (command_value != nullptr) { + len += command_value_len; + } + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + + // command value bytes + if (command_value != nullptr) { + this->write_array(command_value, command_value_len); + } + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE); + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(30); // NOLINT + } + delay(20); // NOLINT +} + +void LD2412Component::handle_periodic_data_() { + // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes + // data header=0xAA, data footer=0x55, crc=0x00 + if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) { + return; + } + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr) { + this->engineering_mode_switch_->publish_state(engineering_mode); + } +#endif + +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = this->buffer_data_[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); + } +#endif + /* + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes + */ +#ifdef USE_SENSOR + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) + if (this->detection_distance_sensor_ != nullptr) { + int new_detect_distance = 0; + if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + } else if (target_state != 0x00) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + } + this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); + } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor: 38th bytes + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + } else { + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + } + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + } +#endif + // the radar module won't tell us when it's done, so we just have to keep polling... + if (this->dynamic_background_correction_active_) { + this->set_config_mode_(true); + this->query_dynamic_background_correction_(); + this->set_config_mode_(false); + } +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + if (n != nullptr && (!n->has_state() || n->state != value)) { + n->state = value; + return [n, value]() { n->publish_state(value); }; + } + return []() {}; +} +#endif + +bool LD2412Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGW(TAG, "Invalid length"); + return true; + } + if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + return true; + } + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGW(TAG, "Invalid status"); + return true; + } + if (this->buffer_data_[8] || this->buffer_data_[9]) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); + return true; + } + + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); + break; + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); + break; + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + } +#endif + break; + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(version); + } +#endif + break; + } + case CMD_QUERY_DISTANCE_RESOLUTION: { + const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + break; + } + + case CMD_QUERY_LIGHT_CONTROL: { + this->light_function_ = this->buffer_data_[10]; + this->light_threshold_ = this->buffer_data_[11]; + const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); + ESP_LOGV(TAG, + "Light function: %s\n" + "Light threshold: %u", + light_function_str, this->light_threshold_); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr) { + this->light_function_select_->publish_state(light_function_str); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr) { + this->light_threshold_number_->publish_state(static_cast(this->light_threshold_)); + } +#endif + break; + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { + return false; + } + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(mac_str); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->bluetooth_on_); + } +#endif + break; + } + + case CMD_SET_DISTANCE_RESOLUTION: + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + + case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: { + ESP_LOGV(TAG, "Handled query dynamic background correction"); + bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00); +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active); + } +#endif + this->dynamic_background_correction_active_ = dynamic_background_correction_active; + break; + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + + case CMD_SET_LIGHT_CONTROL: + ESP_LOGV(TAG, "Handled set light control command"); + break; + + case CMD_QUERY_MOTION_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_STATIC_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_BASIC_CONF: // Query parameters response + { +#ifdef USE_NUMBER + /* + Moving distance range: 9th byte + Still distance range: 10th byte + */ + std::vector> updates; + updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10])); + updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1)); + ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10], + this->buffer_data_[11]); + /* + None Duration: 11~12th bytes + */ + updates.push_back(set_number_value(this->timeout_number_, + ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); + ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + /* + Output pin configuration: 13th bytes + */ + this->out_pin_level_ = this->buffer_data_[14]; +#ifdef USE_SELECT + const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); + if (this->out_pin_level_select_ != nullptr) { + this->out_pin_level_select_->publish_state(out_pin_level_str); + } +#endif + for (auto &update : updates) { + update(); + } +#endif + } break; + default: + break; + } + + return true; +} + +void LD2412Component::readline_(int readch) { + if (readch < 0) { + return; // No data available + } + if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] && + readch != CMD_FRAME_HEADER[this->buffer_pos_]) { + this->buffer_pos_ = 0; + return; + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + this->handle_periodic_data_(); + this->buffer_pos_ = 0; // Reset position index for next message + } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message + } else { + ESP_LOGV(TAG, "Ack Data incomplete"); + } + } +} + +void LD2412Component::set_config_mode_(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); +} + +void LD2412Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_distance_resolution(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2412Component::query_dynamic_background_correction_() { + this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); +} + +void LD2412Component::start_dynamic_background_correction() { + if (this->dynamic_background_correction_active_) { + return; // Already in progress + } +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(true); + } +#endif + this->dynamic_background_correction_active_ = true; + this->set_config_mode_(true); + this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::set_engineering_mode(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->set_config_mode_(true); + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_FACTORY_RESET, nullptr, 0); + this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + +void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); } + +void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } + +void LD2412Component::get_mac_() { + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); +} + +void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } + +void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +void LD2412Component::set_basic_config() { +#ifdef USE_NUMBER + if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } +#endif +#ifdef USE_SELECT + if (!this->out_pin_level_select_->has_state()) { + return; + } +#endif + + uint8_t value[5] = { +#ifdef USE_NUMBER + lowbyte(static_cast(this->min_distance_gate_number_->state)), + lowbyte(static_cast(this->max_distance_gate_number_->state) + 1), + lowbyte(static_cast(this->timeout_number_->state)), + highbyte(static_cast(this->timeout_number_->state)), +#else + 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, +#endif +#ifdef USE_SELECT + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), +#else + 0x01, // Default value if not using select +#endif + }; + this->set_config_mode_(true); + this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); + this->set_config_mode_(false); +} + +#ifdef USE_NUMBER +void LD2412Component::set_gate_threshold() { + if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) { + return; // No gate threshold numbers set; nothing to do here + } + uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + this->set_config_mode_(true); + if (!this->gate_move_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_move_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value)); + } + if (!this->gate_still_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_still_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value)); + } + this->set_config_mode_(false); +} + +void LD2412Component::get_gate_threshold() { + this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0); + this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0); +} + +void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2412Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = static_cast(this->light_threshold_number_->state); + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + } +#endif + uint8_t value[2] = {this->light_function_, this->light_threshold_}; + this->set_config_mode_(true); + this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); + this->query_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +#ifdef USE_SENSOR +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} +void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} +#endif + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h new file mode 100644 index 0000000000..41f96ab301 --- /dev/null +++ b/esphome/components/ld2412/ld2412.h @@ -0,0 +1,141 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace ld2412 { + +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412 + +class LD2412Component : public Component, public uart::UARTDevice { +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(dynamic_background_correction_status) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(light_threshold) + SUB_NUMBER(max_distance_gate) + SUB_NUMBER(min_distance_gate) + SUB_NUMBER(timeout) +#endif +#ifdef USE_SELECT + SUB_SELECT(baud_rate) + SUB_SELECT(distance_resolution) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(bluetooth) + SUB_SWITCH(engineering_mode) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(factory_reset) + SUB_BUTTON(query) + SUB_BUTTON(restart) + SUB_BUTTON(start_dynamic_background_correction) +#endif + + public: + void setup() override; + void dump_config() override; + void loop() override; + void set_light_out_control(); + void set_basic_config(); +#ifdef USE_NUMBER + void set_gate_move_threshold_number(uint8_t gate, number::Number *n); + void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_threshold(); + void get_gate_threshold(); +#endif +#ifdef USE_SENSOR + void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); + void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); +#endif + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const std::string &state); + void set_baud_rate(const std::string &state); + void factory_reset(); + void start_dynamic_background_correction(); + + protected: + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); + void set_config_mode_(bool enable); + void handle_periodic_data_(); + bool handle_ack_data_(); + void readline_(int readch); + void query_parameters_(); + void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void query_light_control_(); + void restart_(); + void query_dynamic_background_correction_(); + + uint8_t light_function_ = 0; + uint8_t light_threshold_ = 0; + uint8_t out_pin_level_ = 0; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + bool bluetooth_on_{false}; + bool dynamic_background_correction_active_{false}; +#ifdef USE_NUMBER + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; +#endif +#ifdef USE_SENSOR + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; +#endif +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py new file mode 100644 index 0000000000..5b0d6d8749 --- /dev/null +++ b/esphome/components/ld2412/number/__init__.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + UNIT_PERCENT, + UNIT_SECOND, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_MAX_DISTANCE_GATE = "max_distance_gate" +CONF_MIN_DISTANCE_GATE = "min_distance_gate" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_TIMEOUT): number.number_schema( + MaxDistanceTimeoutNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + unit_of_measurement=UNIT_SECOND, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_threshold_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE): + n = await number.new_number( + max_distance_gate_config, min_value=2, max_value=13, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_max_distance_gate_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE): + n = await number.new_number( + min_distance_gate_config, min_value=1, max_value=12, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_min_distance_gate_number(n)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_move_threshold_number(x, n)) + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_still_threshold_number(x, n)) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_timeout_number(n)) diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..47f8cd9107 --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -0,0 +1,14 @@ +#include "gate_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h new file mode 100644 index 0000000000..61d9945a0a --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.cpp b/esphome/components/ld2412/number/light_threshold_number.cpp new file mode 100644 index 0000000000..5dab1716bf --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "light_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h new file mode 100644 index 0000000000..d8727d3c98 --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.cpp b/esphome/components/ld2412/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..1c6471bc72 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "max_distance_timeout_number.h" + +namespace esphome { +namespace ld2412 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_basic_config(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..af0dcf68c5 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py new file mode 100644 index 0000000000..d71ce460d9 --- /dev/null +++ b/esphome/components/ld2412/select/__init__.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + CONF_BAUD_RATE, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_RULER, + ICON_SCALE, + ICON_THERMOMETER, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BaudRateSelect = LD2412_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = LD2412_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = LD2412_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_baud_rate_select(s)) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.5m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_distance_resolution_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_function_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_out_pin_level_select(s)) diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp new file mode 100644 index 0000000000..2291a81896 --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2412 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h new file mode 100644 index 0000000000..2ae33551fb --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..a282215fbd --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -0,0 +1,12 @@ +#include "distance_resolution_select.h" + +namespace esphome { +namespace ld2412 { + +void DistanceResolutionSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_distance_resolution(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h new file mode 100644 index 0000000000..0658f5d1a7 --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.cpp b/esphome/components/ld2412/select/light_out_control_select.cpp new file mode 100644 index 0000000000..c331729d40 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.cpp @@ -0,0 +1,12 @@ +#include "light_out_control_select.h" + +namespace esphome { +namespace ld2412 { + +void LightOutControlSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h new file mode 100644 index 0000000000..a71bab1e14 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py new file mode 100644 index 0000000000..abb823faad --- /dev/null +++ b/esphome/components/ld2412/sensor.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_LIGHT, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FLASH, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_SIGNAL, + UNIT_CENTIMETER, + UNIT_EMPTY, + UNIT_PERCENT, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_LIGHTBULB, + unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor + ), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) + cg.add(LD2412_component.set_detection_distance_sensor(sens)) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(LD2412_component.set_light_sensor(sens)) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) + cg.add(LD2412_component.set_moving_target_distance_sensor(sens)) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) + cg.add(LD2412_component.set_moving_target_energy_sensor(sens)) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) + cg.add(LD2412_component.set_still_target_distance_sensor(sens)) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) + cg.add(LD2412_component.set_still_target_energy_sensor(sens)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(LD2412_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(LD2412_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py new file mode 100644 index 0000000000..df994687ec --- /dev/null +++ b/esphome/components/ld2412/switch/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUETOOTH, + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, + ICON_BLUETOOTH, + ICON_PULSE, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BluetoothSwitch = LD2412_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = LD2412_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_bluetooth_switch(s)) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_engineering_mode_switch(s)) diff --git a/esphome/components/ld2412/switch/bluetooth_switch.cpp b/esphome/components/ld2412/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..14387aa276 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2412 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h new file mode 100644 index 0000000000..730d338d87 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..29ca0c22a8 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -0,0 +1,12 @@ +#include "engineering_mode_switch.h" + +namespace esphome { +namespace ld2412 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..aaa404c673 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py new file mode 100644 index 0000000000..1074494933 --- /dev/null +++ b/esphome/components/ld2412/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_BLUETOOTH, + ICON_CHIP, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(LD2412_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(LD2412_component.set_mac_text_sensor(sens)) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b74237ad2e..ac002eac53 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -194,45 +194,7 @@ def final_validate(config): ) -def final_validate_power_esp32_ble(value): - if not CORE.is_esp32: - return - if value != "NONE": - # WiFi should be in modem sleep (!=NONE) with BLE coexistence - # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-guides/wifi.html#station-sleep - return - for conflicting in [ - "esp32_ble", - "esp32_ble_beacon", - "esp32_ble_server", - "esp32_ble_tracker", - ]: - if conflicting not in fv.full_config.get(): - continue - - try: - # Only arduino 1.0.5+ and esp-idf impacted - cv.require_framework_version( - esp32_arduino=cv.Version(1, 0, 5), - esp_idf=cv.Version(4, 0, 0), - )(None) - except cv.Invalid: - pass - else: - raise cv.Invalid( - f"power_save_mode NONE is incompatible with {conflicting}. " - f"Please remove the power save mode. See also " - f"https://github.com/esphome/issues/issues/2141#issuecomment-865688582" - ) - - FINAL_VALIDATE_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_POWER_SAVE_MODE): final_validate_power_esp32_ble, - }, - extra=cv.ALLOW_EXTRA, - ), final_validate, validate_variant, ) diff --git a/esphome/config.py b/esphome/config.py index ecd0cbb048..90325cbf6e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -942,6 +942,9 @@ def validate_config( # do not try to validate further as we don't know what the target is return result + # Reset the pin registry so that any target platforms with pin validations do not get the duplicate pin warning. + pins.PIN_SCHEMA_REGISTRY.reset() + for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) diff --git a/esphome/const.py b/esphome/const.py index 983850746d..49997ca766 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -761,6 +761,7 @@ CONF_POSITION_COMMAND_TOPIC = "position_command_topic" CONF_POSITION_STATE_TOPIC = "position_state_topic" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" +CONF_POWER_MODE = "power_mode" CONF_POWER_ON_VALUE = "power_on_value" CONF_POWER_SAVE_MODE = "power_save_mode" CONF_POWER_SUPPLY = "power_supply" diff --git a/requirements.txt b/requirements.txt index 009a6850e8..9793336cf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==38.1.0 +aioesphomeapi==38.2.1 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml new file mode 100644 index 0000000000..9176c61fd5 --- /dev/null +++ b/tests/components/ld2412/common.yaml @@ -0,0 +1,233 @@ +uart: + - id: uart_ld2412 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +ld2412: + id: my_ld2412 + +binary_sensor: + - platform: ld2412 + dynamic_background_correction_status: + name: Dynamic Background Correction Status + has_target: + name: Presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + +button: + - platform: ld2412 + factory_reset: + name: Factory reset + restart: + name: Restart + query_params: + name: Query params + start_dynamic_background_correction: + name: Start Dynamic Background Correction + +number: + - platform: ld2412 + light_threshold: + name: Light Threshold + timeout: + name: Presence timeout + min_distance_gate: + name: Minimum distance gate + max_distance_gate: + name: Maximum distance gate + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + +select: + - platform: ld2412 + light_function: + name: Light Function + out_pin_level: + name: Hardware output pin level + distance_resolution: + name: Distance resolution + baud_rate: + name: Baud rate + on_value: + - delay: 3s + - lambda: |- + id(uart_ld2412).flush(); + uint32_t new_baud_rate = stoi(x); + ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_ld2412).get_baud_rate(), new_baud_rate); + if (id(uart_ld2412).get_baud_rate() != new_baud_rate) { + id(uart_ld2412).set_baud_rate(new_baud_rate); + #if defined(USE_ESP8266) || defined(USE_ESP32) + id(uart_ld2412).load_settings(); + #endif + } + +sensor: + - platform: ld2412 + light: + name: Light + moving_distance: + name: Moving Distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Detection Distance + gate_0: + move_energy: + name: Gate 0 Move Energy + still_energy: + name: Gate 0 Still Energy + gate_1: + move_energy: + name: Gate 1 Move Energy + still_energy: + name: Gate 1 Still Energy + gate_2: + move_energy: + name: Gate 2 Move Energy + still_energy: + name: Gate 2 Still Energy + gate_3: + move_energy: + name: Gate 3 Move Energy + still_energy: + name: Gate 3 Still Energy + gate_4: + move_energy: + name: Gate 4 Move Energy + still_energy: + name: Gate 4 Still Energy + gate_5: + move_energy: + name: Gate 5 Move Energy + still_energy: + name: Gate 5 Still Energy + gate_6: + move_energy: + name: Gate 6 Move Energy + still_energy: + name: Gate 6 Still Energy + gate_7: + move_energy: + name: Gate 7 Move Energy + still_energy: + name: Gate 7 Still Energy + gate_8: + move_energy: + name: Gate 8 Move Energy + still_energy: + name: Gate 8 Still Energy + gate_9: + move_energy: + name: Gate 9 Move Energy + still_energy: + name: Gate 9 Still Energy + gate_10: + move_energy: + name: Gate 10 Move Energy + still_energy: + name: Gate 10 Still Energy + gate_11: + move_energy: + name: Gate 11 Move Energy + still_energy: + name: Gate 11 Still Energy + gate_12: + move_energy: + name: Gate 12 Move Energy + still_energy: + name: Gate 12 Still Energy + gate_13: + move_energy: + name: Gate 13 Move Energy + still_energy: + name: Gate 13 Still Energy + +switch: + - platform: ld2412 + bluetooth: + name: Bluetooth + engineering_mode: + name: Engineering Mode + +text_sensor: + - platform: ld2412 + version: + name: Firmware version + mac_address: + name: MAC address diff --git a/tests/components/ld2412/test.esp32-ard.yaml b/tests/components/ld2412/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-ard.yaml b/tests/components/ld2412/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-idf.yaml b/tests/components/ld2412/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-idf.yaml b/tests/components/ld2412/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp8266-ard.yaml b/tests/components/ld2412/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.rp2040-ard.yaml b/tests/components/ld2412/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml