diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index e6b6da4177..b2a3394563 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.11.0 + uses: docker/build-push-action@v6.12.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -72,7 +72,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.11.0 + uses: docker/build-push-action@v6.12.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aeb434167a..adf0ac6fc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,14 +11,6 @@ repos: args: [--fix] # Run the formatter. - id: ruff-format - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 - hooks: - - id: black - args: - - --safe - - --quiet - files: ^((esphome|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: @@ -53,6 +45,6 @@ repos: hooks: - id: pylint name: pylint - entry: script/run-in-env.sh pylint - language: script + entry: python script/run-in-env pylint + language: system types: [python] diff --git a/CODEOWNERS b/CODEOWNERS index 92e69e66cb..d3e3de1ffe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/es7210/* @kahrendt esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz @@ -338,7 +339,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet -esphome/components/resistance_sampler/* @jesserockz esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 8f6fc0fb57..929f31e008 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -298,6 +298,12 @@ void DalyBmsComponent::decode_data_(std::vector data) { if (this->cell_16_voltage_sensor_) { this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } + if (this->cell_17_voltage_sensor_) { + this->cell_17_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_18_voltage_sensor_) { + this->cell_18_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } break; } break; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index 52ea30ecde..e6d476bcdd 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -54,6 +54,8 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { SUB_SENSOR(cell_14_voltage) SUB_SENSOR(cell_15_voltage) SUB_SENSOR(cell_16_voltage) + SUB_SENSOR(cell_17_voltage) + SUB_SENSOR(cell_18_voltage) #endif #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index c447fbd8a2..6d78946a02 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -52,6 +52,8 @@ CONF_CELL_13_VOLTAGE = "cell_13_voltage" CONF_CELL_14_VOLTAGE = "cell_14_voltage" CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" +CONF_CELL_17_VOLTAGE = "cell_17_voltage" +CONF_CELL_18_VOLTAGE = "cell_18_voltage" ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" @@ -92,6 +94,8 @@ TYPES = [ CONF_CELL_14_VOLTAGE, CONF_CELL_15_VOLTAGE, CONF_CELL_16_VOLTAGE, + CONF_CELL_17_VOLTAGE, + CONF_CELL_18_VOLTAGE, ] CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( @@ -212,6 +216,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_17_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_18_VOLTAGE): CELL_VOLTAGE_SCHEMA, } ).extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/es7210/__init__.py b/esphome/components/es7210/__init__.py new file mode 100644 index 0000000000..8e63d7f04f --- /dev/null +++ b/esphome/components/es7210/__init__.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["i2c"] + +es7210_ns = cg.esphome_ns.namespace("es7210") +ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice) + + +es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample") +ES7210_BITS_PER_SAMPLE_ENUM = { + 16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16, + 24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24, + 32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32, +} + + +es7210_mic_gain = es7210_ns.enum("ES7210MicGain") +ES7210_MIC_GAIN_ENUM = { + "0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB, + "3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB, + "6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB, + "9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB, + "12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB, + "15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB, + "18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB, + "21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB, + "24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB, + "27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB, + "30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB, + "33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB, + "34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB, + "36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB, + "37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES7210), + cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + _validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM) + ), + cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum( + ES7210_MIC_GAIN_ENUM, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_mic_gain(config[CONF_MIC_GAIN])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp new file mode 100644 index 0000000000..d2f2c3c1ff --- /dev/null +++ b/esphome/components/es7210/es7210.cpp @@ -0,0 +1,201 @@ +#include "es7210.h" +#include "es7210_const.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace es7210 { + +static const char *const TAG = "es7210"; + +static const size_t MCLK_DIV_FRE = 256; + +// Mark the component as failed; use only in setup +#define ES7210_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } + +// Return false; use outside of setup +#define ES7210_ERROR_CHECK(func) \ + if (!(func)) { \ + return false; \ + } + +void ES7210::dump_config() { + ESP_LOGCONFIG(TAG, "ES7210 ADC:"); + ESP_LOGCONFIG(TAG, " Bits Per Sample: %" PRIu8, this->bits_per_sample_); + ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_rate_); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " Failed to initialize!"); + return; + } +} + +void ES7210::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES7210..."); + + // Software reset + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f)); + + // Set initialization time when device powers up + ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30)); + + // Configure HFP for all ADC channels + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a)); + + // Secondary I2S mode settings + ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00)); + + // Configure analog power + ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3)); + + // Set mic bias + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70)); + + // Configure I2S settings, sample rate, and microphone gains + ES7210_ERROR_FAILED(this->configure_i2s_format_()); + ES7210_ERROR_FAILED(this->configure_sample_rate_()); + ES7210_ERROR_FAILED(this->configure_mic_gain_()); + + // Power on mics 1 through 4 + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08)); + + // Power down DLL + ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04)); + + // Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F)); + + // Enable device + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41)); +} + +bool ES7210::configure_sample_rate_() { + int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; + int coeff = -1; + + for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { + if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) + coeff = i; + } + + if (coeff >= 0) { + // Set adc_div & doubler & dll + uint8_t regv; + ES7210_ERROR_CHECK(this->read_byte(ES7210_MAINCLK_REG02, ®v)); + regv = regv & 0x00; + regv |= ES7210_COEFFICIENTS[coeff].adc_div; + regv |= ES7210_COEFFICIENTS[coeff].doubler << 6; + regv |= ES7210_COEFFICIENTS[coeff].dll << 7; + + ES7210_ERROR_CHECK(this->write_byte(ES7210_MAINCLK_REG02, regv)); + + // Set osr + regv = ES7210_COEFFICIENTS[coeff].osr; + ES7210_ERROR_CHECK(this->write_byte(ES7210_OSR_REG07, regv)); + // Set lrck + regv = ES7210_COEFFICIENTS[coeff].lrck_h; + ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVH_REG04, regv)); + regv = ES7210_COEFFICIENTS[coeff].lrck_l; + ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVL_REG05, regv)); + } else { + // Invalid sample frequency + ESP_LOGE(TAG, "Invalid sample rate"); + return false; + } + + return true; +} +bool ES7210::configure_mic_gain_() { + for (int i = 0; i < 4; ++i) { + this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00); + } + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff)); + + // Configure mic 1 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_)); + + // Configure mic 2 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_)); + + // Configure mic 3 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_)); + + // Configure mic 4 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_)); + + return true; +} + +bool ES7210::configure_i2s_format_() { + // Configure bits per sample + uint8_t reg_val = 0; + switch (this->bits_per_sample_) { + case ES7210_BITS_PER_SAMPLE_16: + reg_val = 0x60; + break; + case ES7210_BITS_PER_SAMPLE_18: + reg_val = 0x40; + break; + case ES7210_BITS_PER_SAMPLE_20: + reg_val = 0x20; + break; + case ES7210_BITS_PER_SAMPLE_24: + reg_val = 0x00; + break; + case ES7210_BITS_PER_SAMPLE_32: + reg_val = 0x80; + break; + default: + return false; + } + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE1_REG11, reg_val)); + + if (this->enable_tdm_) { + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x02)); + } else { + // Microphones 1 and 2 output on SDOUT1, microphones 3 and 4 output on SDOUT2 + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x00)); + } + + return true; +} + +bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) { + uint8_t regv; + ES7210_ERROR_CHECK(this->read_byte(reg_addr, ®v)); + regv = (regv & (~update_bits)) | (update_bits & data); + return this->write_byte(reg_addr, regv); +} + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h new file mode 100644 index 0000000000..a40dde5aa5 --- /dev/null +++ b/esphome/components/es7210/es7210.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es7210 { + +enum ES7210BitsPerSample : uint8_t { + ES7210_BITS_PER_SAMPLE_16 = 16, + ES7210_BITS_PER_SAMPLE_18 = 18, + ES7210_BITS_PER_SAMPLE_20 = 20, + ES7210_BITS_PER_SAMPLE_24 = 24, + ES7210_BITS_PER_SAMPLE_32 = 32, +}; + +enum ES7210MicGain : uint8_t { + ES7210_MIC_GAIN_0DB = 0, + ES7210_MIC_GAIN_3DB, + ES7210_MIC_GAIN_6DB, + ES7210_MIC_GAIN_9DB, + ES7210_MIC_GAIN_12DB, + ES7210_MIC_GAIN_15DB, + ES7210_MIC_GAIN_18DB, + ES7210_MIC_GAIN_21DB, + ES7210_MIC_GAIN_24DB, + ES7210_MIC_GAIN_27DB, + ES7210_MIC_GAIN_30DB, + ES7210_MIC_GAIN_33DB, + ES7210_MIC_GAIN_34_5DB, + ES7210_MIC_GAIN_36DB, + ES7210_MIC_GAIN_37_5DB, +}; + +class ES7210 : public Component, public i2c::I2CDevice { + /* Class for configuring an ES7210 ADC for microphone input. + * Based on code from: + * - https://github.com/espressif/esp-bsp/ (accessed 20241219) + * - https://github.com/espressif/esp-adf/ (accessed 20241219) + */ + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + + protected: + /// @brief Updates an I2C registry address by modifying the current state + /// @param reg_addr I2C register address + /// @param update_bits Mask of allowed bits to be modified + /// @param data Bit values to be written + /// @return True if successful, false otherwise + bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data); + + bool configure_i2s_format_(); + bool configure_mic_gain_(); + bool configure_sample_rate_(); + + bool enable_tdm_{false}; // TDM is unsupported in ESPHome as of version 2024.12 + ES7210MicGain mic_gain_; + ES7210BitsPerSample bits_per_sample_; + uint32_t sample_rate_; +}; + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es7210/es7210_const.h b/esphome/components/es7210/es7210_const.h new file mode 100644 index 0000000000..87fd6d86d2 --- /dev/null +++ b/esphome/components/es7210/es7210_const.h @@ -0,0 +1,126 @@ +#pragma once + +#include "es7210.h" + +namespace esphome { +namespace es7210 { + +// ES7210 register addresses +static const uint8_t ES7210_RESET_REG00 = 0x00; /* Reset control */ +static const uint8_t ES7210_CLOCK_OFF_REG01 = 0x01; /* Used to turn off the ADC clock */ +static const uint8_t ES7210_MAINCLK_REG02 = 0x02; /* Set ADC clock frequency division */ + +static const uint8_t ES7210_MASTER_CLK_REG03 = 0x03; /* MCLK source $ SCLK division */ +static const uint8_t ES7210_LRCK_DIVH_REG04 = 0x04; /* lrck_divh */ +static const uint8_t ES7210_LRCK_DIVL_REG05 = 0x05; /* lrck_divl */ +static const uint8_t ES7210_POWER_DOWN_REG06 = 0x06; /* power down */ +static const uint8_t ES7210_OSR_REG07 = 0x07; +static const uint8_t ES7210_MODE_CONFIG_REG08 = 0x08; /* Set primary/secondary & channels */ +static const uint8_t ES7210_TIME_CONTROL0_REG09 = 0x09; /* Set Chip intial state period*/ +static const uint8_t ES7210_TIME_CONTROL1_REG0A = 0x0A; /* Set Power up state period */ +static const uint8_t ES7210_SDP_INTERFACE1_REG11 = 0x11; /* Set sample & fmt */ +static const uint8_t ES7210_SDP_INTERFACE2_REG12 = 0x12; /* Pins state */ +static const uint8_t ES7210_ADC_AUTOMUTE_REG13 = 0x13; /* Set mute */ +static const uint8_t ES7210_ADC34_MUTERANGE_REG14 = 0x14; /* Set mute range */ +static const uint8_t ES7210_ADC12_MUTERANGE_REG15 = 0x15; /* Set mute range */ +static const uint8_t ES7210_ADC34_HPF2_REG20 = 0x20; /* HPF */ +static const uint8_t ES7210_ADC34_HPF1_REG21 = 0x21; /* HPF */ +static const uint8_t ES7210_ADC12_HPF1_REG22 = 0x22; /* HPF */ +static const uint8_t ES7210_ADC12_HPF2_REG23 = 0x23; /* HPF */ +static const uint8_t ES7210_ANALOG_REG40 = 0x40; /* ANALOG Power */ +static const uint8_t ES7210_MIC12_BIAS_REG41 = 0x41; +static const uint8_t ES7210_MIC34_BIAS_REG42 = 0x42; +static const uint8_t ES7210_MIC1_GAIN_REG43 = 0x43; +static const uint8_t ES7210_MIC2_GAIN_REG44 = 0x44; +static const uint8_t ES7210_MIC3_GAIN_REG45 = 0x45; +static const uint8_t ES7210_MIC4_GAIN_REG46 = 0x46; +static const uint8_t ES7210_MIC1_POWER_REG47 = 0x47; +static const uint8_t ES7210_MIC2_POWER_REG48 = 0x48; +static const uint8_t ES7210_MIC3_POWER_REG49 = 0x49; +static const uint8_t ES7210_MIC4_POWER_REG4A = 0x4A; +static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Power */ +static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C; + +/* + * Clock coefficient structer + */ +struct ES7210Coefficient { + uint32_t mclk; // mclk frequency + uint32_t lrclk; + uint8_t ss_ds; + uint8_t adc_div; + uint8_t dll; // dll_bypass + uint8_t doubler; // doubler_enable + uint8_t osr; // adc osr + uint8_t mclk_src; // sselect mclk source + uint8_t lrck_h; // High 4 bits of lrck + uint8_t lrck_l; // Low 8 bits of lrck +}; + +/* Codec hifi mclk clock divider coefficients + * MEMBER REG + * mclk: 0x03 + * lrck: standard + * ss_ds: -- + * adc_div: 0x02 + * dll: 0x06 + * doubler: 0x02 + * osr: 0x07 + * mclk_src: 0x03 + * lrckh: 0x04 + * lrckl: 0x05 + */ +static const ES7210Coefficient ES7210_COEFFICIENTS[] = { + // mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl + /* 8k */ + {12288000, 8000, 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00}, + {16384000, 8000, 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00}, + {19200000, 8000, 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60}, + {4096000, 8000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + + /* 11.025k */ + {11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00}, + + /* 12k */ + {12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00}, + {19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40}, + + /* 16k */ + {4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + {19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80}, + {16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00}, + {12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00}, + + /* 22.05k */ + {11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + + /* 24k */ + {12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + {19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20}, + + /* 32k */ + {12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80}, + {16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + {19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58}, + + /* 44.1k */ + {11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + + /* 48k */ + {12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + {19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90}, + + /* 64k */ + {16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00}, + {19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c}, + + /* 88.2k */ + {11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80}, + + /* 96k */ + {12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80}, + {19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8}, +}; + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py index 1b450c3c11..7d80cfd5fb 100644 --- a/esphome/components/es8311/audio_dac.py +++ b/esphome/components/es8311/audio_dac.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import i2c from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv -from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE CODEOWNERS = ["@kroimon", "@kahrendt"] DEPENDENCIES = ["i2c"] @@ -10,7 +10,6 @@ DEPENDENCIES = ["i2c"] es8311_ns = cg.esphome_ns.namespace("es8311") ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice) -CONF_MIC_GAIN = "mic_gain" CONF_USE_MCLK = "use_mclk" CONF_USE_MICROPHONE = "use_microphone" diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 061afcb026..d27b3b378e 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -8,11 +8,13 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - if (types_.find(event_type) == types_.end()) { + auto found = types_.find(event_type); + if (found == types_.end()) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str()); + last_event_type = &(*found); + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); this->event_callback_.call(event_type); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 067a867360..03c3c8d95a 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -23,6 +23,8 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: + const std::string *last_event_type; + void trigger(const std::string &event_type); void set_event_types(const std::set &event_types) { this->types_ = event_types; } std::set get_event_types() const { return this->types_; } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index b449f046ee..66f064c2ce 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -12,6 +12,8 @@ #include "esp_crt_bundle.h" #endif +#include "esp_task_wdt.h" + namespace esphome { namespace http_request { @@ -117,11 +119,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -151,11 +153,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -185,8 +187,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return 0; } - App.feed_wdt(); + this->feed_wdt(); int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); + this->feed_wdt(); this->bytes_read_ += read_len; this->duration_ms += (millis() - start); @@ -201,6 +204,13 @@ void HttpContainerIDF::end() { esp_http_client_cleanup(this->client_); } +void HttpContainerIDF::feed_wdt() { + // Tests to see if the executing task has a watchdog timer attached + if (esp_task_wdt_status(nullptr) == ESP_OK) { + App.feed_wdt(); + } +} + } // namespace http_request } // namespace esphome diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 431794924b..2ed50698b9 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -18,6 +18,9 @@ class HttpContainerIDF : public HttpContainer { int read(uint8_t *buf, size_t max_len) override; void end() override; + /// @brief Feeds the watchdog timer if the executing task has one attached + void feed_wdt(); + protected: esp_http_client_handle_t client_; }; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0e0966c22b..d683495ac6 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -9,6 +9,13 @@ namespace esphome { namespace http_request { +// The update function runs in a task only on ESP32s. +#ifdef USE_ESP32 +#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task +#else +#define UPDATE_RETURN return +#endif + static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; @@ -29,113 +36,131 @@ void HttpRequestUpdate::setup() { } void HttpRequestUpdate::update() { - auto container = this->request_parent_->get(this->source_url_); +#ifdef USE_ESP32 + xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); +#else + this->update_task(this); +#endif +} + +void HttpRequestUpdate::update_task(void *params) { + HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); - this->status_set_error(msg.c_str()); + this_update->status_set_error(msg.c_str()); container->end(); - return; + UPDATE_RETURN; } size_t read_index = 0; while (container->get_bytes_read() < container->content_length) { int read_bytes = container->read(data + read_index, MAX_READ_SIZE); - App.feed_wdt(); yield(); read_index += read_bytes; } - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); + bool valid = false; + { // Ensures the response string falls out of scope and deallocates before the task ends + std::string response((char *) data, read_index); + allocator.deallocate(data, container->content_length); - container->end(); + container->end(); + container.reset(); // Release ownership of the container's shared_ptr - bool valid = json::parse_json(response, [this](JsonObject root) -> bool { - if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; - } - this->update_info_.title = root["name"].as(); - this->update_info_.latest_version = root["version"].as(); - - for (auto build : root["builds"].as()) { - if (!build.containsKey("chipFamily")) { + valid = json::parse_json(response, [this_update](JsonObject root) -> bool { + if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build.containsKey("ota")) { + this_update->update_info_.title = root["name"].as(); + this_update->update_info_.latest_version = root["version"].as(); + + for (auto build : root["builds"].as()) { + if (!build.containsKey("chipFamily")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - auto ota = build["ota"]; - if (!ota.containsKey("path") || !ota.containsKey("md5")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build["chipFamily"] == ESPHOME_VARIANT) { + if (!build.containsKey("ota")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + auto ota = build["ota"]; + if (!ota.containsKey("path") || !ota.containsKey("md5")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this_update->update_info_.firmware_url = ota["path"].as(); + this_update->update_info_.md5 = ota["md5"].as(); + + if (ota.containsKey("summary")) + this_update->update_info_.summary = ota["summary"].as(); + if (ota.containsKey("release_url")) + this_update->update_info_.release_url = ota["release_url"].as(); + + return true; } - this->update_info_.firmware_url = ota["path"].as(); - this->update_info_.md5 = ota["md5"].as(); - - if (ota.containsKey("summary")) - this->update_info_.summary = ota["summary"].as(); - if (ota.containsKey("release_url")) - this->update_info_.release_url = ota["release_url"].as(); - - return true; } - } - return false; - }); + return false; + }); + } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } - // Merge source_url_ and this->update_info_.firmware_url - if (this->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this->update_info_.firmware_url; + // Merge source_url_ and this_update->update_info_.firmware_url + if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { + std::string path = this_update->update_info_.firmware_url; if (path[0] == '/') { - std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + this_update->update_info_.firmware_url = domain + path; } else { - std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + this_update->update_info_.firmware_url = domain + path; } } - std::string current_version; + { // Ensures the current version string falls out of scope and deallocates before the task ends + std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + current_version = ESPHOME_VERSION; #endif - this->update_info_.current_version = current_version; - - if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) { - this->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - this->state_ = update::UPDATE_STATE_AVAILABLE; + this_update->update_info_.current_version = current_version; } - this->update_info_.has_progress = false; - this->update_info_.progress = 0.0f; + if (this_update->update_info_.latest_version.empty() || + this_update->update_info_.latest_version == this_update->update_info_.current_version) { + this_update->state_ = update::UPDATE_STATE_NO_UPDATE; + } else { + this_update->state_ = update::UPDATE_STATE_AVAILABLE; + } - this->status_clear_error(); - this->publish_state(); + this_update->update_info_.has_progress = false; + this_update->update_info_.progress = 0.0f; + + this_update->status_clear_error(); + this_update->publish_state(); + + UPDATE_RETURN; } void HttpRequestUpdate::perform(bool force) { diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 45c7e6a447..e05fdb0cc2 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -7,6 +7,10 @@ #include "esphome/components/http_request/ota/ota_http_request.h" #include "esphome/components/update/update_entity.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace http_request { @@ -29,6 +33,11 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; std::string source_url_; + + static void update_task(void *params); +#ifdef USE_ESP32 + TaskHandle_t update_task_handle_{nullptr}; +#endif }; } // namespace http_request diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 3c9dd2dab9..e3abb7e98c 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -1,9 +1,12 @@ +import logging + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi from esphome.components.display import validate_rotation import esphome.config_validation as cv from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, CONF_COLOR_ORDER, CONF_COLOR_PALETTE, CONF_DC_PIN, @@ -27,17 +30,12 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import CORE, HexInt +from esphome.final_validate import full_config DEPENDENCIES = ["spi"] - -def AUTO_LOAD(): - if CORE.is_esp32: - return ["psram"] - return [] - - CODEOWNERS = ["@nielsnl68", "@clydebarrow"] +LOGGER = logging.getLogger(__name__) ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( @@ -84,7 +82,7 @@ COLOR_ORDERS = { "BGR": ColorOrder.COLOR_ORDER_BGR, } -COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True) CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" @@ -195,9 +193,27 @@ CONFIG_SCHEMA = cv.All( _validate, ) -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "ili9xxx", require_miso=False, require_mosi=True -) + +def final_validate(config): + global_config = full_config.get() + # Ideally would calculate buffer size here, but that info is not available on the Python side + needs_buffer = ( + CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED] + ) + if ( + CORE.is_esp32 + and config[CONF_COLOR_PALETTE] == "NONE" + and "psram" not in global_config + and needs_buffer + ): + LOGGER.info("Consider enabling PSRAM if available for the display buffer") + + return spi.final_validate_device_schema( + "ili9xxx", require_miso=False, require_mosi=True + ) + + +FINAL_VALIDATE_SCHEMA = final_validate async def to_code(config): @@ -283,6 +299,8 @@ async def to_code(config): palette = converted.getpalette() assert len(palette) == 256 * 3 rhs = palette + elif config[CONF_COLOR_PALETTE] == "8BIT": + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8)) else: cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16)) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index b9664067a9..f056f0a128 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() { void ILI9XXXDisplay::alloc_buffer_() { if (this->buffer_color_mode_ == BITS_16) { this->init_internal_(this->get_buffer_length_() * 2); - if (this->buffer_ != nullptr) { - return; - } - this->buffer_color_mode_ = BITS_8; + } else { + this->init_internal_(this->get_buffer_length_()); } - this->init_internal_(this->get_buffer_length_()); if (this->buffer_ == nullptr) { this->mark_failed(); } diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index c141739d2a..87d7c86e5c 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -98,7 +98,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer, protected: inline bool check_buffer_() { if (this->buffer_ == nullptr) { - this->alloc_buffer_(); + if (!this->is_failed()) + this->alloc_buffer_(); return !this->is_failed(); } return true; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 801b05e160..a503e8f471 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -82,11 +82,13 @@ class ImageEncoder: self.dither = dither self.index = 0 self.invert_alpha = invert_alpha + self.path = "" - def convert(self, image): + def convert(self, image, path): """ Convert the image format :param image: Input image + :param path: Path to the image file :return: converted image """ return image @@ -103,6 +105,16 @@ class ImageEncoder: """ +def is_alpha_only(image: Image): + """ + Check if an image (assumed to be RGBA) is only alpha + """ + # Any alpha data? + if image.split()[-1].getextrema()[0] == 0xFF: + return False + return all(b.getextrema()[1] == 0 for b in image.split()[:-1]) + + class ImageBinary(ImageEncoder): allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY} @@ -111,7 +123,9 @@ class ImageBinary(ImageEncoder): super().__init__(self.width8, height, transparency, dither, invert_alpha) self.bitno = 0 - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + image = image.split()[-1] return image.convert("1", dither=self.dither) def encode(self, pixel): @@ -136,7 +150,16 @@ class ImageBinary(ImageEncoder): class ImageGrayscale(ImageEncoder): allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + if self.transparency != CONF_ALPHA_CHANNEL: + _LOGGER.warning( + "Grayscale image %s is alpha only, but transparency is set to %s", + path, + self.transparency, + ) + self.transparency = CONF_ALPHA_CHANNEL + image = image.split()[-1] return image.convert("LA") def encode(self, pixel): @@ -166,7 +189,7 @@ class ImageRGB565(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -204,7 +227,7 @@ class ImageRGB(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -308,7 +331,7 @@ def is_svg_file(file): if not file: return False with open(file, "rb") as f: - return "get_grayscale_pixel_(img_x, img_y); - if (color.w >= 0x80) { - display->draw_pixel_at(x + img_x, y + img_y, color); + const uint32_t pos = (img_x + img_y * this->width_); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); + Color color = Color(gray, gray, gray, 0xFF); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) { + continue; // skip drawing + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: { + auto on = (float) gray / 255.0f; + auto off = 1.0f - on; + // blend color_on and color_off + color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off, + color_on.b * on + color_off.b * off, 0xFF); + break; + } + default: + break; } + display->draw_pixel_at(x + img_x, y + img_y, color); } } break; @@ -179,8 +196,16 @@ Color Image::get_rgb565_pixel_(int x, int y) const { Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) + return Color(0, 0, 0, 0); + return Color(gray, gray, gray, 0xFF); + case TRANSPARENCY_ALPHA_CHANNEL: + return Color(0, 0, 0, gray); + default: + return Color(gray, gray, gray, 0xFF); + } } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 7db6e1f045..168fc03cb7 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -15,6 +15,7 @@ from .defines import ( CONF_FREEZE, CONF_LVGL_ID, CONF_SHOW_SNOW, + PARTS, literal, ) from .lv_validation import lv_bool, lv_color, lv_image, opacity @@ -33,7 +34,7 @@ from .lvcode import ( lvgl_comp, static_cast, ) -from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA +from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema from .types import ( LV_STATE, LvglAction, @@ -41,6 +42,7 @@ from .types import ( ObjUpdateAction, lv_disp_t, lv_group_t, + lv_obj_base_t, lv_obj_t, lv_pseudo_button_t, ) @@ -336,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args): lv.group_focus_freeze(group, True) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) return var + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS) +) +async def obj_update_to_code(config, action_id, template_arg, args): + async def do_update(widget: Widget): + await set_obj_properties(widget, config) + + widgets = await get_widgets(config[CONF_ID]) + return await action_to_code(widgets, do_update, action_id, template_arg, args) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 271dbea19f..f0318dd17a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -199,13 +199,12 @@ FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FL FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) -def part_schema(widget_type: WidgetType): +def part_schema(parts): """ Generate a schema for the various parts (e.g. main:, indicator:) of a widget type - :param widget_type: The type of widget to generate for - :return: + :param parts: The parts to include in the schema + :return: The schema """ - parts = widget_type.parts return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( STATE_SCHEMA ) @@ -228,9 +227,15 @@ def automation_schema(typ: LvType): } -def create_modify_schema(widget_type): +def base_update_schema(widget_type, parts): + """ + Create a schema for updating a widgets style properties, states and flags + :param widget_type: The type of the ID + :param parts: The allowable parts to specify + :return: + """ return ( - part_schema(widget_type) + part_schema(parts) .extend( { cv.Required(CONF_ID): cv.ensure_list( @@ -245,7 +250,12 @@ def create_modify_schema(widget_type): } ) .extend(FLAG_SCHEMA) - .extend(widget_type.modify_schema) + ) + + +def create_modify_schema(widget_type): + return base_update_schema(widget_type.w_type, widget_type.parts).extend( + widget_type.modify_schema ) @@ -256,7 +266,7 @@ def obj_schema(widget_type: WidgetType): :return: """ return ( - part_schema(widget_type) + part_schema(widget_type.parts) .extend(FLAG_SCHEMA) .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) @@ -341,7 +351,6 @@ FLEX_OBJ_SCHEMA = { cv.Optional(df.CONF_FLEX_GROW): cv.int_, } - DISP_BG_SCHEMA = cv.Schema( { cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py index a6bfc6bb88..b32b5a2b2e 100644 --- a/esphome/components/lvgl/widgets/dropdown.py +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -37,7 +37,7 @@ DROPDOWN_BASE_SCHEMA = cv.Schema( cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int, cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, - cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), + cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts), } ) diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index ba7edb302e..d4a71078d0 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -16,6 +16,11 @@ KEYBOARD_SCHEMA = { cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), } +KEYBOARD_MODIFY_SCHEMA = { + cv.Optional(CONF_MODE): KEYBOARD_MODES.one_of, + cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), +} + lv_keyboard_t = LvType( "LvKeyboardType", parents=(KeyProvider, LvCompound), @@ -32,6 +37,7 @@ class KeyboardType(WidgetType): lv_keyboard_t, (CONF_MAIN, CONF_ITEMS), KEYBOARD_SCHEMA, + modify_schema=KEYBOARD_MODIFY_SCHEMA, ) def get_uses(self): @@ -41,7 +47,8 @@ class KeyboardType(WidgetType): lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add(CONF_KEYBOARD) add_lv_use("btnmatrix") - await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) + if mode := config.get(CONF_MODE): + await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) if ta := await get_widgets(config, CONF_TEXTAREA): await w.set_property(CONF_TEXTAREA, ta[0].obj) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index c3393940b6..82b2442378 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema( cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), - cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts), cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), } diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py index afb4c97f33..ab22a5ce86 100644 --- a/esphome/components/lvgl/widgets/obj.py +++ b/esphome/components/lvgl/widgets/obj.py @@ -1,9 +1,5 @@ -from esphome import automation - -from ..automation import update_to_code from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR -from ..schemas import create_modify_schema -from ..types import ObjUpdateAction, WidgetType, lv_obj_t +from ..types import WidgetType, lv_obj_t class ObjType(WidgetType): @@ -21,10 +17,3 @@ class ObjType(WidgetType): obj_spec = ObjType() - - -@automation.register_action( - "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) -) -async def obj_update_to_code(config, action_id, template_arg, args): - return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 226fc3f286..1d18ddd259 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -38,7 +38,7 @@ TABVIEW_SCHEMA = cv.Schema( }, ) ), - cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts), cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, cv.Optional(CONF_SIZE, default="10%"): size, } diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index 961511fe00..bd5f4a1841 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -2,7 +2,7 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor, resistance_sampler +from esphome.components import sensor from esphome.const import ( CONF_CALIBRATION, CONF_REFERENCE_RESISTANCE, @@ -15,8 +15,6 @@ from esphome.const import ( UNIT_CELSIUS, ) -AUTO_LOAD = ["resistance_sampler"] - ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) @@ -126,7 +124,7 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_CALIBRATION): process_calibration, } ) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 5d1861202a..2d39d8ef3f 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -59,6 +59,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->text_sensor_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_NUMBER + this->number_type_(stream); + for (auto *obj : App.get_numbers()) + this->number_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_SELECT + this->select_type_(stream); + for (auto *obj : App.get_selects()) + this->select_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_MEDIA_PLAYER + this->media_player_type_(stream); + for (auto *obj : App.get_media_players()) + this->media_player_row_(stream, obj, area, node, friendly_name); +#endif + req->send(stream); } @@ -511,6 +529,156 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso } #endif +// Type-specific implementation +#ifdef USE_NUMBER +void PrometheusHandler::number_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_number_value gauge\n")); + stream->print(F("#TYPE esphome_number_failed gauge\n")); +} +void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, + std::string &node, std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (!std::isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_number_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_number_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print(F("\n")); + } else { + // Invalid state + stream->print(F("esphome_number_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SELECT +void PrometheusHandler::select_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_select_value gauge\n")); + stream->print(F("#TYPE esphome_select_failed gauge\n")); +} +void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, + std::string &node, std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(F("esphome_select_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_select_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + } else { + // Invalid state + stream->print(F("esphome_select_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_MEDIA_PLAYER +void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); + stream->print(F("#TYPE esphome_media_player_volume gauge\n")); + stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); + stream->print(F("#TYPE esphome_media_player_failed gauge\n")); +} +void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, + std::string &area, std::string &node, std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + stream->print(F("esphome_media_player_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_media_player_state_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\",value=\"")); + stream->print(media_player::media_player_state_to_string(obj->state)); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + stream->print(F("esphome_media_player_volume{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} ")); + stream->print(obj->volume); + stream->print(F("\n")); + stream->print(F("esphome_media_player_is_muted{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(F("\"} ")); + if (obj->is_muted()) { + stream->print(F("1.0")); + } else { + stream->print(F("0.0")); + } + stream->print(F("\n")); +} +#endif + } // namespace prometheus } // namespace esphome #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5d08aca63a..41a06537ed 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -128,6 +128,30 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_NUMBER + /// Return the type for prometheus + void number_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_SELECT + /// Return the type for prometheus + void select_type_(AsyncResponseStream *stream); + /// Return the select state as prometheus data point + void select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_MEDIA_PLAYER + /// Return the type for prometheus + void media_player_type_(AsyncResponseStream *stream); + /// Return the select state as prometheus data point + void media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area, + std::string &node, std::string &friendly_name); +#endif + web_server_base::WebServerBase *base_; bool include_internal_{false}; std::map relabel_map_id_; diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index 8fa1f8b570..b57f90b59c 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -1,8 +1,7 @@ #pragma once -#include "esphome/components/resistance_sampler/resistance_sampler.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" namespace esphome { namespace resistance { @@ -12,7 +11,7 @@ enum ResistanceConfiguration { DOWNSTREAM, }; -class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler { +class ResistanceSensor : public Component, public sensor::Sensor { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index ce4459fc6d..3622799a07 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, resistance_sampler +from esphome.components import sensor from esphome.const import ( CONF_REFERENCE_VOLTAGE, CONF_SENSOR, @@ -9,15 +9,8 @@ from esphome.const import ( ICON_FLASH, ) -AUTO_LOAD = ["resistance_sampler"] - resistance_ns = cg.esphome_ns.namespace("resistance") -ResistanceSensor = resistance_ns.class_( - "ResistanceSensor", - cg.Component, - sensor.Sensor, - resistance_sampler.ResistanceSampler, -) +ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) CONF_CONFIGURATION = "configuration" CONF_RESISTOR = "resistor" diff --git a/esphome/components/resistance_sampler/__init__.py b/esphome/components/resistance_sampler/__init__.py deleted file mode 100644 index d2032848aa..0000000000 --- a/esphome/components/resistance_sampler/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import esphome.codegen as cg - -resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler") -ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler") - -CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/resistance_sampler/resistance_sampler.h b/esphome/components/resistance_sampler/resistance_sampler.h deleted file mode 100644 index 9e300bebcc..0000000000 --- a/esphome/components/resistance_sampler/resistance_sampler.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace esphome { -namespace resistance_sampler { - -/// Abstract interface to mark components that provide resistance values. -class ResistanceSampler {}; - -} // namespace resistance_sampler -} // namespace esphome diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index f9435b0424..b13826c443 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -7,6 +7,10 @@ namespace spi { const char *const TAG = "spi"; +SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + new SPIDelegateDummy(); +// https://bugs.llvm.org/show_bug.cgi?id=48040 + bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -75,6 +79,8 @@ void SPIComponent::dump_config() { } } +void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } + uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 4cd8d3383c..f581dc3f56 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -163,6 +163,8 @@ class Utility { } }; +class SPIDelegateDummy; + // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is // a thin wrapper over SPIClass. class SPIDelegate { @@ -248,6 +250,21 @@ class SPIDelegate { uint32_t data_rate_{1000000}; SPIMode mode_{MODE0}; GPIOPin *cs_pin_{NullPin::NULL_PIN}; + static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +}; + +/** + * A dummy SPIDelegate that complains if it's used. + */ + +class SPIDelegateDummy : public SPIDelegate { + public: + SPIDelegateDummy() = default; + + uint8_t transfer(uint8_t data) override { return 0; } + void end_transaction() override{}; + + void begin_transaction() override; }; /** @@ -365,7 +382,7 @@ class SPIClient { virtual void spi_teardown() { this->parent_->unregister_device(this); - this->delegate_ = nullptr; + this->delegate_ = SPIDelegate::NULL_DELEGATE; } bool spi_is_ready() { return this->delegate_->is_ready(); } @@ -376,7 +393,7 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; - SPIDelegate *delegate_{nullptr}; + SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; }; /** diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor/__init__.py similarity index 100% rename from esphome/components/uptime/sensor.py rename to esphome/components/uptime/sensor/__init__.py index 30220751b6..e2a7aee1a2 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -1,14 +1,14 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, time +import esphome.config_validation as cv from esphome.const import ( CONF_TIME_ID, + DEVICE_CLASS_DURATION, DEVICE_CLASS_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, + ICON_TIMER, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, - ICON_TIMER, - DEVICE_CLASS_DURATION, ) uptime_ns = cg.esphome_ns.namespace("uptime") diff --git a/esphome/components/uptime/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.cpp rename to esphome/components/uptime/sensor/uptime_seconds_sensor.cpp diff --git a/esphome/components/uptime/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.h rename to esphome/components/uptime/sensor/uptime_seconds_sensor.h diff --git a/esphome/components/uptime/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.cpp rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp diff --git a/esphome/components/uptime/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.h rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.h diff --git a/esphome/components/uptime/text_sensor/__init__.py b/esphome/components/uptime/text_sensor/__init__.py new file mode 100644 index 0000000000..996d983e71 --- /dev/null +++ b/esphome/components/uptime/text_sensor/__init__.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER + +uptime_ns = cg.esphome_ns.namespace("uptime") +UptimeTextSensor = uptime_ns.class_( + "UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent +) +CONFIG_SCHEMA = text_sensor.text_sensor_schema( + UptimeTextSensor, + icon=ICON_TIMER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp new file mode 100644 index 0000000000..0fa5e199f3 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -0,0 +1,46 @@ +#include "uptime_text_sensor.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uptime { + +static const char *const TAG = "uptime.sensor"; + +void UptimeTextSensor::setup() { this->last_ms_ = millis(); } + +void UptimeTextSensor::update() { + const auto now = millis(); + // get whole seconds since last update. Note that even if the millis count has overflowed between updates, + // the difference will still be correct due to the way twos-complement arithmetic works. + const uint32_t delta = (now - this->last_ms_) / 1000; + if (delta == 0) + return; + // set last_ms_ to the last second boundary + this->last_ms_ = now - (now % 1000); + this->uptime_ += delta; + auto uptime = this->uptime_; + unsigned days = uptime / (24 * 3600); + unsigned seconds = uptime % (24 * 3600); + unsigned hours = seconds / 3600; + seconds %= 3600; + unsigned minutes = seconds / 60; + seconds %= 60; + if (days != 0) { + this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds)); + } else if (hours != 0) { + this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds)); + } else if (minutes != 0) { + this->publish_state(str_sprintf("%dm%ds", minutes, seconds)); + } else { + this->publish_state(str_sprintf("%ds", seconds)); + } +} + +float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h new file mode 100644 index 0000000000..4baf1039b6 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uptime { + +class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override; + void dump_config() override; + void setup() override; + + float get_setup_priority() const override; + + protected: + uint64_t uptime_{0}; + uint64_t last_ms_{0}; +}; + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ed0cb3db2c..8c09d607a7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -455,8 +455,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method == "toggle") { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on") { - auto call = obj->turn_on(); + } else if (match.method == "turn_on" || match.method == "turn_off") { + auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off(); + if (request->hasParam("speed_level")) { auto speed_level = request->getParam("speed_level")->value(); auto val = parse_number(speed_level.c_str()); @@ -486,9 +487,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->schedule_([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method == "turn_off") { - this->schedule_([obj]() { obj->turn_off().perform(); }); - request->send(200); } else { request->send(404); } diff --git a/esphome/const.py b/esphome/const.py index 0f41dc1aec..284f8d5f78 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.1.0-dev" +__version__ = "2025.2.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -490,6 +490,7 @@ CONF_MEMORY_BLOCKS = "memory_blocks" CONF_MESSAGE = "message" CONF_METHANE = "methane" CONF_METHOD = "method" +CONF_MIC_GAIN = "mic_gain" CONF_MICROPHONE = "microphone" CONF_MIN_BRIGHTNESS = "min_brightness" CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" diff --git a/script/run-in-env b/script/run-in-env new file mode 100644 index 0000000000..57bfddccf7 --- /dev/null +++ b/script/run-in-env @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +import subprocess +import sys + + +def find_and_activate_virtualenv(): + try: + # Get the top-level directory of the git repository + my_path = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True + ).strip() + except subprocess.CalledProcessError: + print( + "Error: Not a git repository or unable to determine the top-level directory.", + file=sys.stderr, + ) + sys.exit(1) + + # Check for virtual environments + for venv in ["venv", ".venv", "."]: + activate_path = ( + Path(my_path) + / venv + / ("Scripts" if os.name == "nt" else "bin") + / "activate" + ) + if activate_path.exists(): + # Activate the virtual environment by updating PATH + env = os.environ.copy() + venv_bin_dir = activate_path.parent + env["PATH"] = f"{venv_bin_dir}{os.pathsep}{env['PATH']}" + env["VIRTUAL_ENV"] = str(venv_bin_dir.parent) + print(f"Activated virtual environment: {venv_bin_dir.parent}") + + # Execute the remaining arguments in the new environment + if len(sys.argv) > 1: + subprocess.run(sys.argv[1:], env=env, check=False) + else: + print( + "No command provided to run in the virtual environment.", + file=sys.stderr, + ) + return + + print("No virtual environment found.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + find_and_activate_virtualenv() diff --git a/script/run-in-env.sh b/script/run-in-env.sh deleted file mode 100755 index 2e05fe1d17..0000000000 --- a/script/run-in-env.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env sh -set -eu - -my_path=$(git rev-parse --show-toplevel) - -for venv in venv .venv .; do - if [ -f "${my_path}/${venv}/bin/activate" ]; then - . "${my_path}/${venv}/bin/activate" - break - fi -done - -exec "$@" diff --git a/tests/components/es7210/common.yaml b/tests/components/es7210/common.yaml new file mode 100644 index 0000000000..5c30f7e883 --- /dev/null +++ b/tests/components/es7210/common.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_aic3204 + scl: ${scl_pin} + sda: ${sda_pin} + +es7210: diff --git a/tests/components/es7210/test.esp32-ard.yaml b/tests/components/es7210/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7210/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-ard.yaml b/tests/components/es7210/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7210/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7210/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-idf.yaml b/tests/components/es7210/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7210/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml index 850273230a..c00c38ce3e 100644 --- a/tests/components/ili9xxx/test.esp32-ard.yaml +++ b/tests/components/ili9xxx/test.esp32-ard.yaml @@ -20,6 +20,7 @@ display: it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx invert_colors: false + color_palette: 8bit dimensions: width: 320 height: 240 diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7c59cfa171..b3227bb96e 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -805,9 +805,24 @@ lvgl: - logger.log: format: "keyboard value %s" args: [text.c_str()] + - lvgl.keyboard.update: + id: lv_keyboard + hidden: true + on_ready: + - lvgl.widget.update: + id: lv_keyboard + - lvgl.keyboard.update: + id: lv_keyboard + hidden: true + - keyboard: id: lv_keyboard1 mode: special + on_ready: + lvgl.keyboard.update: + id: lv_keyboard1 + hidden: true + mode: text_lower font: - file: "gfonts://Roboto" diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 68ef2a2f58..1b87c1d6c1 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -78,6 +78,26 @@ lock: } optimistic: true +select: + - platform: template + id: template_select1 + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +number: + - platform: template + id: template_number1 + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + prometheus: include_internal: true relabel: diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml index dade44d145..3045a6db13 100644 --- a/tests/components/prometheus/test.esp32-ard.yaml +++ b/tests/components/prometheus/test.esp32-ard.yaml @@ -1 +1,34 @@ <<: !include common.yaml + +i2s_audio: + i2s_lrclk_pin: 1 + i2s_bclk_pin: 2 + i2s_mclk_pin: 3 + +media_player: + - platform: i2s_audio + name: "Media Player" + dac_type: external + i2s_dout_pin: 18 + mute_pin: 19 + on_state: + - media_player.play: + - media_player.play_media: http://localhost/media.mp3 + - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' + on_idle: + - media_player.pause: + on_play: + - media_player.stop: + on_pause: + - media_player.toggle: + - wait_until: + media_player.is_idle: + - wait_until: + media_player.is_playing: + - wait_until: + media_player.is_announcing: + - wait_until: + media_player.is_paused: + - media_player.volume_up: + - media_player.volume_down: + - media_player.volume_set: 50% diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index f63f80b050..d78ef8eca9 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -13,3 +13,7 @@ sensor: - platform: uptime name: Uptime Sensor Timestamp type: timestamp + +text_sensor: + - platform: uptime + name: Uptime Text