diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index cc9894a657..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.10.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.10.0 + uses: docker/build-push-action@v6.12.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 435a58498e..b994cfaf17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -46,9 +46,9 @@ jobs: with: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set TAG run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ce4159da0..a344b177ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ on: - ".github/workflows/ci.yml" - "!.yamllint" - "!.github/dependabot.yml" + - "!docker/**" merge_group: permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 096b00f0f1..962bc66e94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.2 + uses: pypa/gh-action-pypi-publish@v1.12.3 deploy-docker: name: Build ESPHome ${{ matrix.platform }} @@ -90,10 +90,10 @@ jobs: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU if: matrix.platform != 'linux/amd64' - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Log in to docker hub uses: docker/login-action@v3.3.0 @@ -141,7 +141,7 @@ jobs: echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT - name: Upload digests - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.0 with: name: digests-${{ steps.sanitize.outputs.name }} path: /tmp/digests @@ -184,7 +184,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 7a46d596a1..9160ab4a1b 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v7.0.5 + uses: peter-evans/create-pull-request@v7.0.6 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot 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 74c205b302..f0075549fd 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 @@ -302,7 +303,7 @@ esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb -esphome/components/online_image/* @guillempages +esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core @@ -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 @@ -355,6 +355,7 @@ esphome/components/sdl/* @clydebarrow esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/seeed_mr24hpc1/* @limengdu +esphome/components/seeed_mr60bha2/* @limengdu esphome/components/seeed_mr60fda2/* @limengdu esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core diff --git a/README.md b/README.md index da1b2b3650..8e3d8f71aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) -[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/) + + + + ESPHome Logo + + **Documentation:** https://esphome.io/ diff --git a/docker/Dockerfile b/docker/Dockerfile index 0bb558d35e..429f5c4a1f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,7 @@ RUN \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ python3-pip=23.0.1+dfsg-1 \ - python3-setuptools=66.1.1-1 \ + python3-setuptools=66.1.1-1+deb12u1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1+deb12u1 \ diff --git a/esphome/__main__.py b/esphome/__main__.py index dce041e5ac..2a0bd8f2b3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -758,6 +758,14 @@ def parse_args(argv): options_parser.add_argument( "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" ) + options_parser.add_argument( + "-l", + "--log-level", + help="Set the log level.", + default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"), + action="store", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + ) options_parser.add_argument( "--dashboard", help=argparse.SUPPRESS, action="store_true" ) @@ -987,11 +995,16 @@ def run_esphome(argv): args = parse_args(argv) CORE.dashboard = args.dashboard + # Override log level if verbose is set + if args.verbose: + args.log_level = "DEBUG" + elif args.quiet: + args.log_level = "CRITICAL" + setup_log( - args.verbose, - args.quiet, + log_level=args.log_level, # Show timestamp for dashboard access logs - args.command == "dashboard", + include_timestamp=args.command == "dashboard", ) if args.command in PRE_CONFIG_ACTIONS: diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 11b0ba2389..d8d21523b9 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,11 +1,6 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER - -from esphome.core import CORE +import esphome.codegen as cg from esphome.components.esp32 import get_esp32_variant -from esphome.const import PLATFORM_ESP8266 from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C2, @@ -15,6 +10,9 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +import esphome.config_validation as cv +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -102,11 +100,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 6: adc1_channel_t.ADC1_CHANNEL_6, }, VARIANT_ESP32H2: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, }, } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index b697d6dd7e..7a3e1c8da7 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -3,13 +3,12 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/hal.h" #ifdef USE_ESP32 #include #include "driver/adc.h" -#endif +#endif // USE_ESP32 namespace esphome { namespace adc { @@ -43,7 +42,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage this->channel1_ = ADC1_CHANNEL_MAX; } void set_autorange(bool autorange) { this->autorange_ = autorange; } -#endif +#endif // USE_ESP32 /// Update ADC values void update() override; @@ -59,11 +58,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP8266 std::string unique_id() override; -#endif +#endif // USE_ESP8266 #ifdef USE_RP2040 void set_is_temperature() { this->is_temperature_ = true; } -#endif +#endif // USE_RP2040 protected: InternalGPIOPin *pin_; @@ -72,7 +71,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_RP2040 bool is_temperature_{false}; -#endif +#endif // USE_RP2040 #ifdef USE_ESP32 adc_atten_t attenuation_{ADC_ATTEN_DB_0}; @@ -83,8 +82,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; #else esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; -#endif -#endif +#endif // ESP_IDF_VERSION_MAJOR +#endif // USE_ESP32 }; } // namespace adc diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp new file mode 100644 index 0000000000..2dccd55fcd --- /dev/null +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -0,0 +1,24 @@ +#include "adc_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adc { + +static const char *const TAG = "adc.common"; + +void ADCSensor::update() { + float value_v = this->sample(); + ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + this->publish_state(value_v); +} + +void ADCSensor::set_sample_count(uint8_t sample_count) { + if (sample_count != 0) { + this->sample_count_ = sample_count; + } +} + +float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace adc +} // namespace esphome diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor_esp32.cpp similarity index 53% rename from esphome/components/adc/adc_sensor.cpp rename to esphome/components/adc/adc_sensor_esp32.cpp index 7257793016..24e3750091 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -1,30 +1,13 @@ +#ifdef USE_ESP32 + #include "adc_sensor.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESP8266 -#ifdef USE_ADC_SENSOR_VCC -#include -ADC_MODE(ADC_VCC) -#else -#include -#endif -#endif - -#ifdef USE_RP2040 -#ifdef CYW43_USES_VSYS_PIN -#include "pico/cyw43_arch.h" -#endif -#include -#endif - namespace esphome { namespace adc { -static const char *const TAG = "adc"; +static const char *const TAG = "adc.esp32"; -// 13-bit for S2, 12-bit for all other ESP32 variants -#ifdef USE_ESP32 static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast(ADC_WIDTH_MAX - 1); #ifndef SOC_ADC_RTC_MAX_BITWIDTH @@ -32,24 +15,15 @@ static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast> 1; // 2048 (12 bit) or 4096 (13 bit) -#endif +static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; +static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; -#ifdef USE_RP2040 -extern "C" -#endif - void - ADCSensor::setup() { +void ADCSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); -#if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040) - this->pin_->setup(); -#endif -#ifdef USE_ESP32 if (this->channel1_ != ADC1_CHANNEL_MAX) { adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); if (!this->autorange_) { @@ -61,7 +35,6 @@ extern "C" } } - // load characteristics for each attenuation for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, @@ -79,31 +52,10 @@ extern "C" break; } } - -#endif // USE_ESP32 - -#ifdef USE_RP2040 - static bool initialized = false; - if (!initialized) { - adc_init(); - initialized = true; - } -#endif - - ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str()); } void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) -#ifdef USE_ADC_SENSOR_VCC - ESP_LOGCONFIG(TAG, " Pin: VCC"); -#else - LOG_PIN(" Pin: ", this->pin_); -#endif -#endif // USE_ESP8266 || USE_LIBRETINY - -#ifdef USE_ESP32 LOG_PIN(" Pin: ", this->pin_); if (this->autorange_) { ESP_LOGCONFIG(TAG, " Attenuation: auto"); @@ -125,55 +77,10 @@ void ADCSensor::dump_config() { break; } } -#endif // USE_ESP32 - -#ifdef USE_RP2040 - if (this->is_temperature_) { - ESP_LOGCONFIG(TAG, " Pin: Temperature"); - } else { -#ifdef USE_ADC_SENSOR_VCC - ESP_LOGCONFIG(TAG, " Pin: VCC"); -#else - LOG_PIN(" Pin: ", this->pin_); -#endif // USE_ADC_SENSOR_VCC - } -#endif // USE_RP2040 ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); LOG_UPDATE_INTERVAL(this); } -float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } -void ADCSensor::update() { - float value_v = this->sample(); - ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); - this->publish_state(value_v); -} - -void ADCSensor::set_sample_count(uint8_t sample_count) { - if (sample_count != 0) { - this->sample_count_ = sample_count; - } -} - -#ifdef USE_ESP8266 -float ADCSensor::sample() { - uint32_t raw = 0; - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { -#ifdef USE_ADC_SENSOR_VCC - raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) -#else - raw += analogRead(this->pin_->get_pin()); // NOLINT -#endif - } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - if (this->output_raw_) { - return raw; - } - return raw / 1024.0f; -} -#endif - -#ifdef USE_ESP32 float ADCSensor::sample() { if (!this->autorange_) { uint32_t sum = 0; @@ -240,93 +147,17 @@ float ADCSensor::sample() { uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); - // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) uint32_t c12 = std::min(raw12, ADC_HALF); uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); - // max theoretical csum value is 4096*4 = 16384 uint32_t csum = c12 + c6 + c2 + c0; - // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); return mv_scaled / (float) (csum * 1000U); } -#endif // USE_ESP32 - -#ifdef USE_RP2040 -float ADCSensor::sample() { - if (this->is_temperature_) { - adc_set_temp_sensor_enabled(true); - delay(1); - adc_select_input(4); - uint32_t raw = 0; - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += adc_read(); - } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - adc_set_temp_sensor_enabled(false); - if (this->output_raw_) { - return raw; - } - return raw * 3.3f / 4096.0f; - } else { - uint8_t pin = this->pin_->get_pin(); -#ifdef CYW43_USES_VSYS_PIN - if (pin == PICO_VSYS_PIN) { - // Measuring VSYS on Raspberry Pico W needs to be wrapped with - // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in - // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and - // VSYS ADC both share GPIO29 - cyw43_thread_enter(); - } -#endif // CYW43_USES_VSYS_PIN - - adc_gpio_init(pin); - adc_select_input(pin - 26); - - uint32_t raw = 0; - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += adc_read(); - } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - -#ifdef CYW43_USES_VSYS_PIN - if (pin == PICO_VSYS_PIN) { - cyw43_thread_exit(); - } -#endif // CYW43_USES_VSYS_PIN - - if (this->output_raw_) { - return raw; - } - float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0; - return raw * 3.3f / 4096.0f * coeff; - } -} -#endif - -#ifdef USE_LIBRETINY -float ADCSensor::sample() { - uint32_t raw = 0; - if (this->output_raw_) { - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += analogRead(this->pin_->get_pin()); // NOLINT - } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - return raw; - } - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT - } - raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) - return raw / 1000.0f; -} -#endif // USE_LIBRETINY - -#ifdef USE_ESP8266 -std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } -#endif } // namespace adc } // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/adc/adc_sensor_esp8266.cpp b/esphome/components/adc/adc_sensor_esp8266.cpp new file mode 100644 index 0000000000..c9b6f8b652 --- /dev/null +++ b/esphome/components/adc/adc_sensor_esp8266.cpp @@ -0,0 +1,58 @@ +#ifdef USE_ESP8266 + +#include "adc_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ADC_SENSOR_VCC +#include +ADC_MODE(ADC_VCC) +#else +#include +#endif // USE_ADC_SENSOR_VCC + +namespace esphome { +namespace adc { + +static const char *const TAG = "adc.esp8266"; + +void ADCSensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); +#ifndef USE_ADC_SENSOR_VCC + this->pin_->setup(); +#endif +} + +void ADCSensor::dump_config() { + LOG_SENSOR("", "ADC Sensor", this); +#ifdef USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Pin: VCC"); +#else + LOG_PIN(" Pin: ", this->pin_); +#endif // USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + LOG_UPDATE_INTERVAL(this); +} + +float ADCSensor::sample() { + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { +#ifdef USE_ADC_SENSOR_VCC + raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) +#else + raw += analogRead(this->pin_->get_pin()); // NOLINT +#endif // USE_ADC_SENSOR_VCC + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + if (this->output_raw_) { + return raw; + } + return raw / 1024.0f; +} + +std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } + +} // namespace adc +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/adc/adc_sensor_libretiny.cpp b/esphome/components/adc/adc_sensor_libretiny.cpp new file mode 100644 index 0000000000..cd04477b3f --- /dev/null +++ b/esphome/components/adc/adc_sensor_libretiny.cpp @@ -0,0 +1,48 @@ +#ifdef USE_LIBRETINY + +#include "adc_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adc { + +static const char *const TAG = "adc.libretiny"; + +void ADCSensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); +#ifndef USE_ADC_SENSOR_VCC + this->pin_->setup(); +#endif // !USE_ADC_SENSOR_VCC +} + +void ADCSensor::dump_config() { + LOG_SENSOR("", "ADC Sensor", this); +#ifdef USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Pin: VCC"); +#else // USE_ADC_SENSOR_VCC + LOG_PIN(" Pin: ", this->pin_); +#endif // USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + LOG_UPDATE_INTERVAL(this); +} + +float ADCSensor::sample() { + uint32_t raw = 0; + if (this->output_raw_) { + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += analogRead(this->pin_->get_pin()); // NOLINT + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + return raw; + } + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + return raw / 1000.0f; +} + +} // namespace adc +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp new file mode 100644 index 0000000000..520ff3bacc --- /dev/null +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -0,0 +1,93 @@ +#ifdef USE_RP2040 + +#include "adc_sensor.h" +#include "esphome/core/log.h" + +#ifdef CYW43_USES_VSYS_PIN +#include "pico/cyw43_arch.h" +#endif // CYW43_USES_VSYS_PIN +#include + +namespace esphome { +namespace adc { + +static const char *const TAG = "adc.rp2040"; + +void ADCSensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); + static bool initialized = false; + if (!initialized) { + adc_init(); + initialized = true; + } +} + +void ADCSensor::dump_config() { + LOG_SENSOR("", "ADC Sensor", this); + if (this->is_temperature_) { + ESP_LOGCONFIG(TAG, " Pin: Temperature"); + } else { +#ifdef USE_ADC_SENSOR_VCC + ESP_LOGCONFIG(TAG, " Pin: VCC"); +#else + LOG_PIN(" Pin: ", this->pin_); +#endif // USE_ADC_SENSOR_VCC + } + ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_); + LOG_UPDATE_INTERVAL(this); +} + +float ADCSensor::sample() { + if (this->is_temperature_) { + adc_set_temp_sensor_enabled(true); + delay(1); + adc_select_input(4); + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += adc_read(); + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + adc_set_temp_sensor_enabled(false); + if (this->output_raw_) { + return raw; + } + return raw * 3.3f / 4096.0f; + } + + uint8_t pin = this->pin_->get_pin(); +#ifdef CYW43_USES_VSYS_PIN + if (pin == PICO_VSYS_PIN) { + // Measuring VSYS on Raspberry Pico W needs to be wrapped with + // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in + // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and + // VSYS ADC both share GPIO29 + cyw43_thread_enter(); + } +#endif // CYW43_USES_VSYS_PIN + + adc_gpio_init(pin); + adc_select_input(pin - 26); + + uint32_t raw = 0; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + raw += adc_read(); + } + raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero) + +#ifdef CYW43_USES_VSYS_PIN + if (pin == PICO_VSYS_PIN) { + cyw43_thread_exit(); + } +#endif // CYW43_USES_VSYS_PIN + + if (this->output_raw_) { + return raw; + } + float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f; + return raw * 3.3f / 4096.0f * coeff; +} + +} // namespace adc +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 21a82649f0..f73b8ef08f 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,28 +1,10 @@ import logging -from esphome import automation, core +from esphome import automation import esphome.codegen as cg import esphome.components.image as espImage -from esphome.components.image import ( - CONF_USE_TRANSPARENCY, - LOCAL_SCHEMA, - SOURCE_LOCAL, - SOURCE_WEB, - WEB_SCHEMA, -) import esphome.config_validation as cv -from esphome.const import ( - CONF_FILE, - CONF_ID, - CONF_PATH, - CONF_RAW_DATA_ID, - CONF_REPEAT, - CONF_RESIZE, - CONF_SOURCE, - CONF_TYPE, - CONF_URL, -) -from esphome.core import CORE, HexInt +from esphome.const import CONF_ID, CONF_REPEAT _LOGGER = logging.getLogger(__name__) @@ -30,6 +12,7 @@ AUTO_LOAD = ["image"] CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True CONF_LOOP = "loop" CONF_START_FRAME = "start_frame" @@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_( "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) ) -TYPED_FILE_SCHEMA = cv.typed_schema( +CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( { - SOURCE_LOCAL: LOCAL_SCHEMA, - SOURCE_WEB: WEB_SCHEMA, - }, - key=CONF_SOURCE, -) - - -def _file_schema(value): - if isinstance(value, str): - return validate_file_shorthand(value) - return TYPED_FILE_SCHEMA(value) - - -FILE_SCHEMA = cv.Schema(_file_schema) - - -def validate_file_shorthand(value): - value = cv.string_strict(value) - if value.startswith("http://") or value.startswith("https://"): - return FILE_SCHEMA( + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Optional(CONF_LOOP): cv.All( { - CONF_SOURCE: SOURCE_WEB, - CONF_URL: value, + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, } - ) - return FILE_SCHEMA( - { - CONF_SOURCE: SOURCE_LOCAL, - CONF_PATH: value, - } - ) - - -def validate_cross_dependencies(config): - """ - Validate fields whose possible values depend on other fields. - For example, validate that explicitly transparent image types - have "use_transparency" set to True. - Also set the default value for those kind of dependent fields. - """ - image_type = config[CONF_TYPE] - is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] - # If the use_transparency option was not specified, set the default depending on the image type - if CONF_USE_TRANSPARENCY not in config: - config[CONF_USE_TRANSPARENCY] = is_transparent_type - - if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: - raise cv.Invalid(f"Image type {image_type} must always be transparent.") - - return config - - -ANIMATION_SCHEMA = cv.Schema( - cv.All( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( - espImage.IMAGE_TYPE, upper=True - ), - # Not setting default here on purpose; the default depends on the image type, - # and thus will be set in the "validate_cross_dependencies" validator. - cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, - cv.Optional(CONF_LOOP): cv.All( - { - cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, - cv.Optional(CONF_END_FRAME): cv.positive_int, - cv.Optional(CONF_REPEAT): cv.positive_int, - } - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - }, - validate_cross_dependencies, - ) + ), + }, ) -CONFIG_SCHEMA = ANIMATION_SCHEMA NEXT_FRAME_SCHEMA = automation.maybe_simple_id( { @@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args): async def to_code(config): - from PIL import Image + ( + prog_arr, + width, + height, + image_type, + trans_value, + frame_count, + ) = await espImage.write_image(config, all_frames=True) - conf_file = config[CONF_FILE] - if conf_file[CONF_SOURCE] == SOURCE_LOCAL: - path = CORE.relative_config_path(conf_file[CONF_PATH]) - elif conf_file[CONF_SOURCE] == SOURCE_WEB: - path = espImage.compute_local_image_path(conf_file).as_posix() - else: - raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}") - - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") - - width, height = image.size - frames = image.n_frames - if CONF_RESIZE in config: - new_width_max, new_height_max = config[CONF_RESIZE] - ratio = min(new_width_max / width, new_height_max / height) - width, height = int(width * ratio), int(height * ratio) - elif width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) - - transparent = config[CONF_USE_TRANSPARENCY] - - if config[CONF_TYPE] == "GRAYSCALE": - data = [0 for _ in range(height * width * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("LA", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix, a in pixels: - if transparent: - if pix == 1: - pix = 0 - if a < 0x80: - pix = 1 - - data[pos] = pix - pos += 1 - - elif config[CONF_TYPE] == "RGBA": - data = [0 for _ in range(height * width * 4 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix in pixels: - data[pos] = pix[0] - pos += 1 - data[pos] = pix[1] - pos += 1 - data[pos] = pix[2] - pos += 1 - data[pos] = pix[3] - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - data = [0 for _ in range(height * width * 3 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range((height * width8 // 8) * frames)] - for frameIndex in range(frames): - image.seek(frameIndex) - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - else: - has_alpha = False - frame = image.convert("1", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - if transparent: - alpha = alpha.resize([width, height]) - for x, y in [(i, j) for i in range(width) for j in range(height)]: - if transparent and has_alpha: - if not alpha.getpixel((x, y)): - continue - elif frame.getpixel((x, y)): - continue - - pos = x + y * width8 + (height * width8 * frameIndex) - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, - frames, - espImage.IMAGE_TYPE[config[CONF_TYPE]], + frame_count, + image_type, + trans_value, ) - cg.add(var.set_transparency(transparent)) if loop_config := config.get(CONF_LOOP): start = loop_config[CONF_START_FRAME] - end = loop_config.get(CONF_END_FRAME, frames) + end = loop_config.get(CONF_END_FRAME, frame_count) count = loop_config.get(CONF_REPEAT, -1) cg.add(var.set_loop(start, end, count)) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 1375dfe07e..6db6f1a7bd 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -6,8 +6,8 @@ namespace esphome { namespace animation { Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, - image::ImageType type) - : Image(data_start, width, height, type), + image::ImageType type, image::Transparency transparent) + : Image(data_start, width, height, type, transparent), animation_data_start_(data_start), current_frame_(0), animation_frame_count_(animation_frame_count), diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index 272c5153d1..c44e0060af 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -8,7 +8,8 @@ namespace animation { class Animation : public image::Image { public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, + image::Transparency transparent); uint32_t get_animation_frame_count() const; int get_current_frame() const; diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 19cf2bc1f3..5cf096c9d4 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -25,8 +25,7 @@ void BLEClient::loop() { void BLEClient::dump_config() { ESP_LOGCONFIG(TAG, "BLE Client:"); - ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); - ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_)); + BLEClientBase::dump_config(); } bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 543752853e..b63f7ccde9 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -13,6 +13,11 @@ namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; +void BluetoothConnection::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Connection:"); + BLEClientBase::dump_config(); +} + bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index e6ab3cbccc..fd83f8dd00 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -11,6 +11,7 @@ class BluetoothProxy; class BluetoothConnection : public esp32_ble_client::BLEClientBase { public: + void dump_config() override; bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 76adfb42bb..8175383627 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -37,8 +37,9 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 22b3431c3e..5c6bfd7740 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei } else { parent->mode = climate::CLIMATE_MODE_FAN_ONLY; } - } else + } else { parent->mode = climate::CLIMATE_MODE_COOL; + } // Fan Speed if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || 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/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index cbd4249d92..7d25bf5472 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -50,6 +50,10 @@ void DebugComponent::dump_config() { this->reset_reason_->publish_state(get_reset_reason_()); } #endif // USE_TEXT_SENSOR + +#ifdef USE_ESP32 + this->log_partition_info_(); // Log partition information for ESP32 +#endif // USE_ESP32 } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 2b54406603..608addb4a3 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent { #endif // USE_ESP32 #endif // USE_SENSOR +#ifdef USE_ESP32 + /** + * @brief Logs information about the device's partition table. + * + * This function iterates through the ESP32's partition table and logs details + * about each partition, including its name, type, subtype, starting address, + * and size. The information is useful for diagnosing issues related to flash + * memory or verifying the partition configuration dynamically at runtime. + * + * Only available when compiled for ESP32 platforms. + */ + void log_partition_info_(); +#endif // USE_ESP32 + #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *reset_reason_{nullptr}; diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index cb4330f422..69ae7e3678 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(USE_ESP32_VARIANT_ESP32) #include @@ -28,111 +29,177 @@ namespace debug { static const char *const TAG = "debug"; +void DebugComponent::log_partition_info_() { + ESP_LOGCONFIG(TAG, "Partition table:"); + ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it != NULL) { + const esp_partition_t *partition = esp_partition_get(it); + ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype, + partition->address, partition->size); + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); +} + std::string DebugComponent::get_reset_reason_() { std::string reset_reason; - switch (rtc_get_reset_reason(0)) { - case POWERON_RESET: - reset_reason = "Power On Reset"; + switch (esp_reset_reason()) { + case ESP_RST_POWERON: + reset_reason = "Reset due to power-on event"; break; + case ESP_RST_EXT: + reset_reason = "Reset by external pin"; + break; + case ESP_RST_SW: + reset_reason = "Software reset via esp_restart"; + break; + case ESP_RST_PANIC: + reset_reason = "Software reset due to exception/panic"; + break; + case ESP_RST_INT_WDT: + reset_reason = "Reset (software or hardware) due to interrupt watchdog"; + break; + case ESP_RST_TASK_WDT: + reset_reason = "Reset due to task watchdog"; + break; + case ESP_RST_WDT: + reset_reason = "Reset due to other watchdogs"; + break; + case ESP_RST_DEEPSLEEP: + reset_reason = "Reset after exiting deep sleep mode"; + break; + case ESP_RST_BROWNOUT: + reset_reason = "Brownout reset (software or hardware)"; + break; + case ESP_RST_SDIO: + reset_reason = "Reset over SDIO"; + break; +#ifdef USE_ESP32_VARIANT_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)) + case ESP_RST_USB: + reset_reason = "Reset by USB peripheral"; + break; + case ESP_RST_JTAG: + reset_reason = "Reset by JTAG"; + break; + case ESP_RST_EFUSE: + reset_reason = "Reset due to efuse error"; + break; + case ESP_RST_PWR_GLITCH: + reset_reason = "Reset due to power glitch detected"; + break; + case ESP_RST_CPU_LOCKUP: + reset_reason = "Reset due to CPU lock up (double exception)"; + break; +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4) +#endif // USE_ESP32_VARIANT_ESP32 + default: // Includes ESP_RST_UNKNOWN + switch (rtc_get_reset_reason(0)) { + case POWERON_RESET: + reset_reason = "Power On Reset"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case SW_RESET: + case SW_RESET: #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case RTC_SW_SYS_RESET: + case RTC_SW_SYS_RESET: #endif - reset_reason = "Software Reset Digital Core"; - break; + reset_reason = "Software Reset Digital Core"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case OWDT_RESET: - reset_reason = "Watch Dog Reset Digital Core"; - break; + case OWDT_RESET: + reset_reason = "Watch Dog Reset Digital Core"; + break; #endif - case DEEPSLEEP_RESET: - reset_reason = "Deep Sleep Reset Digital Core"; - break; + case DEEPSLEEP_RESET: + reset_reason = "Deep Sleep Reset Digital Core"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case SDIO_RESET: - reset_reason = "SLC Module Reset Digital Core"; - break; + case SDIO_RESET: + reset_reason = "SLC Module Reset Digital Core"; + break; #endif - case TG0WDT_SYS_RESET: - reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; - break; - case TG1WDT_SYS_RESET: - reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; - break; - case RTCWDT_SYS_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core"; - break; + case TG0WDT_SYS_RESET: + reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; + break; + case TG1WDT_SYS_RESET: + reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; + break; + case RTCWDT_SYS_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core"; + break; #if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) - case INTRUSION_RESET: - reset_reason = "Intrusion Reset CPU"; - break; + case INTRUSION_RESET: + reset_reason = "Intrusion Reset CPU"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32) - case TGWDT_CPU_RESET: - reset_reason = "Timer Group Reset CPU"; - break; + case TGWDT_CPU_RESET: + reset_reason = "Timer Group Reset CPU"; + break; #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case TG0WDT_CPU_RESET: - reset_reason = "Timer Group 0 Reset CPU"; - break; + case TG0WDT_CPU_RESET: + reset_reason = "Timer Group 0 Reset CPU"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32) - case SW_CPU_RESET: + case SW_CPU_RESET: #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case RTC_SW_CPU_RESET: + case RTC_SW_CPU_RESET: #endif - reset_reason = "Software Reset CPU"; - break; - case RTCWDT_CPU_RESET: - reset_reason = "RTC Watch Dog Reset CPU"; - break; + reset_reason = "Software Reset CPU"; + break; + case RTCWDT_CPU_RESET: + reset_reason = "RTC Watch Dog Reset CPU"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case EXT_CPU_RESET: - reset_reason = "External CPU Reset"; - break; + case EXT_CPU_RESET: + reset_reason = "External CPU Reset"; + break; #endif - case RTCWDT_BROWN_OUT_RESET: - reset_reason = "Voltage Unstable Reset"; - break; - case RTCWDT_RTC_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; - break; + case RTCWDT_BROWN_OUT_RESET: + reset_reason = "Voltage Unstable Reset"; + break; + case RTCWDT_RTC_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; + break; #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32C6) - case TG1WDT_CPU_RESET: - reset_reason = "Timer Group 1 Reset CPU"; - break; - case SUPER_WDT_RESET: - reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; - break; - case EFUSE_RESET: - reset_reason = "eFuse Reset Digital Core"; - break; + case TG1WDT_CPU_RESET: + reset_reason = "Timer Group 1 Reset CPU"; + break; + case SUPER_WDT_RESET: + reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; + break; + case EFUSE_RESET: + reset_reason = "eFuse Reset Digital Core"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - case GLITCH_RTC_RESET: - reset_reason = "Glitch Reset Digital Core And RTC Module"; - break; + case GLITCH_RTC_RESET: + reset_reason = "Glitch Reset Digital Core And RTC Module"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case USB_UART_CHIP_RESET: - reset_reason = "USB UART Reset Digital Core"; - break; - case USB_JTAG_CHIP_RESET: - reset_reason = "USB JTAG Reset Digital Core"; - break; + case USB_UART_CHIP_RESET: + reset_reason = "USB UART Reset Digital Core"; + break; + case USB_JTAG_CHIP_RESET: + reset_reason = "USB JTAG Reset Digital Core"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) - case POWER_GLITCH_RESET: - reset_reason = "Power Glitch Reset Digital Core And RTC Module"; - break; + case POWER_GLITCH_RESET: + reset_reason = "Power Glitch Reset Digital Core And RTC Module"; + break; #endif - default: - reset_reason = "Unknown Reset Reason"; + default: + reset_reason = "Unknown Reset Reason"; + } + break; } ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); return reset_reason; @@ -223,6 +290,19 @@ void DebugComponent::get_device_info_(std::string &device_info) { device_info += " Cores:" + to_string(info.cores); device_info += " Revision:" + to_string(info.revision); + // Framework detection + device_info += "|Framework: "; +#ifdef USE_ARDUINO + ESP_LOGD(TAG, "Framework: Arduino"); + device_info += "Arduino"; +#elif defined(USE_ESP_IDF) + ESP_LOGD(TAG, "Framework: ESP-IDF"); + device_info += "ESP-IDF"; +#else + ESP_LOGW(TAG, "Framework: UNKNOWN"); + device_info += "UNKNOWN"; +#endif + ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); device_info += "|ESP-IDF: "; device_info += esp_get_idf_version(); @@ -294,4 +374,4 @@ void DebugComponent::update_platform_() { } // namespace debug } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 98c3e91e46..70bd42e1a5 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -159,6 +159,15 @@ void DFPlayer::loop() { } break; case 9: // End byte +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char byte_sequence[100]; + byte_sequence[0] = '\0'; + for (size_t i = 0; i < this->read_pos_ + 1; ++i) { + snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", + this->read_buffer_[i]); + } + ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); +#endif if (byte != 0xEF) { ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); this->read_pos_ = 0; @@ -238,13 +247,17 @@ void DFPlayer::loop() { this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; break; + case 0x3C: + ESP_LOGV(TAG, "Playback finished (USB drive)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); case 0x3D: - ESP_LOGV(TAG, "Playback finished"); + ESP_LOGV(TAG, "Playback finished (SD card)"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); break; default: - ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); + ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); } this->sent_cmd_ = 0; this->read_pos_ = 0; diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp index f8ef6c7138..f47025698b 100644 --- a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -118,8 +118,9 @@ std::unique_ptr CircularCommandQueue::dequeue() { if (front_ == rear_) { front_ = -1; rear_ = -1; - } else + } else { front_ = (front_ + 1) % COMMAND_QUEUE_SIZE; + } return dequeued_cmd; } diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 3f9f9c57f4..5a18f6f36e 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -157,8 +157,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r if (bit == 0) { bit = 7; byte++; - } else + } else { bit--; + } } } if (!report_errors && error_code != 0) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 32a8b3b090..99224df7b3 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_( CONF_ON_PAGE_CHANGE = "on_page_change" CONF_SHOW_TEST_CARD = "show_test_card" +CONF_UNSPECIFIED = "unspecified" DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -55,16 +56,22 @@ def validate_rotation(value): return cv.enum(DISPLAY_ROTATIONS, int=True)(value) +def validate_auto_clear(value): + if value == CONF_UNSPECIFIED: + return value + return cv.boolean(value) + + BASIC_DISPLAY_SCHEMA = cv.Schema( { - cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_, } ).extend(cv.polling_component_schema("1s")) FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { cv.Optional(CONF_ROTATION): validate_rotation, - cv.Optional(CONF_PAGES): cv.All( + cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All( cv.ensure_list( { cv.GenerateID(): cv.declare_id(DisplayPage), @@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_TO): cv.use_id(DisplayPage), } ), - cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + cv.Optional( + CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED + ): validate_auto_clear, cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) @@ -92,8 +101,12 @@ async def setup_display_core_(var, config): if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) - if CONF_AUTO_CLEAR_ENABLED in config: - cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) + if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED): + # Default to true if pages or lambda is specified. Ideally this would be done during validation, but + # the possible schemas are too complex to do this easily. + if auto_clear == CONF_UNSPECIFIED: + auto_clear = CONF_LAMBDA in config or CONF_PAGES in config + cg.add(var.set_auto_clear(auto_clear)) if CONF_PAGES in config: pages = [] diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 1d996bd59b..202c64ef14 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -1,6 +1,6 @@ #include "display.h" -#include "display_color_utils.h" #include +#include "display_color_utils.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -266,8 +266,9 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (dymax < float(-dxmax) * tan_a) { upd_dxmax = ceil(float(dymax) / tan_a); hline_width = -dxmax - upd_dxmax + 1; - } else + } else { hline_width = 0; + } } if (hline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); @@ -670,7 +671,7 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou this->print(x, y, font, color, align, buffer, background); } void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { - this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); + this->strftime(x, y, font, color, COLOR_OFF, align, format, time); } void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp index 34b611191f..49bb7d025f 100644 --- a/esphome/components/display/rect.cpp +++ b/esphome/components/display/rect.cpp @@ -90,8 +90,9 @@ void Rect::info(const std::string &prefix) { if (this->is_set()) { ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), this->y2()); - } else + } else { ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); + } } } // namespace display 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/esp32/__init__.py b/esphome/components/esp32/__init__.py index b0bde75451..98db45831a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -602,6 +602,9 @@ async def to_code(config): cg.add_platformio_option( "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] ) + add_idf_sdkconfig_option( + f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True + ) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option( diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 02744ecb6f..81400eb9c3 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,4 +1,12 @@ -from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3 +from .const import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) ESP32_BASE_PINS = { "TX": 1, @@ -1344,6 +1352,26 @@ done | sort """ BOARDS = { + "4d_systems_esp32s3_gen4_r8n16": { + "name": "4D Systems GEN4-ESP32 16MB (ESP32S3-R8N16)", + "variant": VARIANT_ESP32S3, + }, + "adafruit_camera_esp32s3": { + "name": "Adafruit pyCamera S3", + "variant": VARIANT_ESP32S3, + }, + "adafruit_feather_esp32c6": { + "name": "Adafruit Feather ESP32-C6", + "variant": VARIANT_ESP32C6, + }, + "adafruit_feather_esp32s2": { + "name": "Adafruit Feather ESP32-S2", + "variant": VARIANT_ESP32S2, + }, + "adafruit_feather_esp32s2_reversetft": { + "name": "Adafruit Feather ESP32-S2 Reverse TFT", + "variant": VARIANT_ESP32S2, + }, "adafruit_feather_esp32s2_tft": { "name": "Adafruit Feather ESP32-S2 TFT", "variant": VARIANT_ESP32S2, @@ -1356,6 +1384,10 @@ BOARDS = { "name": "Adafruit Feather ESP32-S3 No PSRAM", "variant": VARIANT_ESP32S3, }, + "adafruit_feather_esp32s3_reversetft": { + "name": "Adafruit Feather ESP32-S3 Reverse TFT", + "variant": VARIANT_ESP32S3, + }, "adafruit_feather_esp32s3_tft": { "name": "Adafruit Feather ESP32-S3 TFT", "variant": VARIANT_ESP32S3, @@ -1376,10 +1408,18 @@ BOARDS = { "name": "Adafruit MagTag 2.9", "variant": VARIANT_ESP32S2, }, + "adafruit_matrixportal_esp32s3": { + "name": "Adafruit MatrixPortal ESP32-S3", + "variant": VARIANT_ESP32S3, + }, "adafruit_metro_esp32s2": { "name": "Adafruit Metro ESP32-S2", "variant": VARIANT_ESP32S2, }, + "adafruit_metro_esp32s3": { + "name": "Adafruit Metro ESP32-S3", + "variant": VARIANT_ESP32S3, + }, "adafruit_qtpy_esp32c3": { "name": "Adafruit QT Py ESP32-C3", "variant": VARIANT_ESP32C3, @@ -1392,10 +1432,18 @@ BOARDS = { "name": "Adafruit QT Py ESP32-S2", "variant": VARIANT_ESP32S2, }, + "adafruit_qtpy_esp32s3_n4r2": { + "name": "Adafruit QT Py ESP32-S3 (4M Flash 2M PSRAM)", + "variant": VARIANT_ESP32S3, + }, "adafruit_qtpy_esp32s3_nopsram": { "name": "Adafruit QT Py ESP32-S3 No PSRAM", "variant": VARIANT_ESP32S3, }, + "adafruit_qualia_s3_rgb666": { + "name": "Adafruit Qualia ESP32-S3 RGB666", + "variant": VARIANT_ESP32S3, + }, "airm2m_core_esp32c3": { "name": "AirM2M CORE ESP32C3", "variant": VARIANT_ESP32C3, @@ -1404,14 +1452,30 @@ BOARDS = { "name": "ALKS ESP32", "variant": VARIANT_ESP32, }, + "arduino_nano_esp32": { + "name": "Arduino Nano ESP32", + "variant": VARIANT_ESP32S3, + }, + "atd147_s3": { + "name": "ArtronShop ATD1.47-S3", + "variant": VARIANT_ESP32S3, + }, "atmegazero_esp32s2": { "name": "EspinalLab ATMegaZero ESP32-S2", "variant": VARIANT_ESP32S2, }, + "aventen_s3_sync": { + "name": "Aventen S3 Sync", + "variant": VARIANT_ESP32S3, + }, "az-delivery-devkit-v4": { "name": "AZ-Delivery ESP-32 Dev Kit C V4", "variant": VARIANT_ESP32, }, + "bee_data_logger": { + "name": "Smart Bee Data Logger", + "variant": VARIANT_ESP32S3, + }, "bee_motion_mini": { "name": "Smart Bee Motion Mini", "variant": VARIANT_ESP32C3, @@ -1436,14 +1500,6 @@ BOARDS = { "name": "BPI-Leaf-S3", "variant": VARIANT_ESP32S3, }, - "briki_abc_esp32": { - "name": "Briki ABC (MBC-WB) - ESP32", - "variant": VARIANT_ESP32, - }, - "briki_mbc-wb_esp32": { - "name": "Briki MBC-WB - ESP32", - "variant": VARIANT_ESP32, - }, "cnrs_aw2eth": { "name": "CNRS AW2ETH", "variant": VARIANT_ESP32, @@ -1496,18 +1552,38 @@ BOARDS = { "name": "DFRobot Beetle ESP32-C3", "variant": VARIANT_ESP32C3, }, + "dfrobot_firebeetle2_esp32e": { + "name": "DFRobot Firebeetle 2 ESP32-E", + "variant": VARIANT_ESP32, + }, "dfrobot_firebeetle2_esp32s3": { "name": "DFRobot Firebeetle 2 ESP32-S3", "variant": VARIANT_ESP32S3, }, + "dfrobot_romeo_esp32s3": { + "name": "DFRobot Romeo ESP32-S3", + "variant": VARIANT_ESP32S3, + }, "dpu_esp32": { "name": "TAMC DPU ESP32", "variant": VARIANT_ESP32, }, + "edgebox-esp-100": { + "name": "Seeed Studio Edgebox-ESP-100", + "variant": VARIANT_ESP32S3, + }, "esp320": { "name": "Electronic SweetPeas ESP320", "variant": VARIANT_ESP32, }, + "esp32-c2-devkitm-1": { + "name": "Espressif ESP32-C2-DevKitM-1", + "variant": VARIANT_ESP32C2, + }, + "esp32-c3-devkitc-02": { + "name": "Espressif ESP32-C3-DevKitC-02", + "variant": VARIANT_ESP32C3, + }, "esp32-c3-devkitm-1": { "name": "Espressif ESP32-C3-DevKitM-1", "variant": VARIANT_ESP32C3, @@ -1516,6 +1592,14 @@ BOARDS = { "name": "Ai-Thinker ESP-C3-M1-I-Kit", "variant": VARIANT_ESP32C3, }, + "esp32-c6-devkitc-1": { + "name": "Espressif ESP32-C6-DevKitC-1", + "variant": VARIANT_ESP32C6, + }, + "esp32-c6-devkitm-1": { + "name": "Espressif ESP32-C6-DevKitM-1", + "variant": VARIANT_ESP32C6, + }, "esp32cam": { "name": "AI Thinker ESP32-CAM", "variant": VARIANT_ESP32, @@ -1544,6 +1628,14 @@ BOARDS = { "name": "OLIMEX ESP32-GATEWAY", "variant": VARIANT_ESP32, }, + "esp32-h2-devkitm-1": { + "name": "Espressif ESP32-H2-DevKit", + "variant": VARIANT_ESP32H2, + }, + "esp32-pico-devkitm-2": { + "name": "Espressif ESP32-PICO-DevKitM-2", + "variant": VARIANT_ESP32, + }, "esp32-poe-iso": { "name": "OLIMEX ESP32-PoE-ISO", "variant": VARIANT_ESP32, @@ -1580,10 +1672,22 @@ BOARDS = { "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)", "variant": VARIANT_ESP32S3, }, - "esp32-s3-korvo-2": { - "name": "Espressif ESP32-S3-Korvo-2", + "esp32-s3-devkitm-1": { + "name": "Espressif ESP32-S3-DevKitM-1", "variant": VARIANT_ESP32S3, }, + "esp32s3_powerfeather": { + "name": "ESP32-S3 PowerFeather", + "variant": VARIANT_ESP32S3, + }, + "esp32s3usbotg": { + "name": "Espressif ESP32-S3-USB-OTG", + "variant": VARIANT_ESP32S3, + }, + "esp32-solo1": { + "name": "Espressif Generic ESP32-solo1 4M Flash", + "variant": VARIANT_ESP32, + }, "esp32thing": { "name": "SparkFun ESP32 Thing", "variant": VARIANT_ESP32, @@ -1652,9 +1756,9 @@ BOARDS = { "name": "Heltec WiFi Kit 32", "variant": VARIANT_ESP32, }, - "heltec_wifi_kit_32_v2": { - "name": "Heltec WiFi Kit 32 (V2)", - "variant": VARIANT_ESP32, + "heltec_wifi_kit_32_V3": { + "name": "Heltec WiFi Kit 32 (V3)", + "variant": VARIANT_ESP32S3, }, "heltec_wifi_lora_32": { "name": "Heltec WiFi LoRa 32", @@ -1664,6 +1768,10 @@ BOARDS = { "name": "Heltec WiFi LoRa 32 (V2)", "variant": VARIANT_ESP32, }, + "heltec_wifi_lora_32_V3": { + "name": "Heltec WiFi LoRa 32 (V3)", + "variant": VARIANT_ESP32S3, + }, "heltec_wireless_stick_lite": { "name": "Heltec Wireless Stick Lite", "variant": VARIANT_ESP32, @@ -1708,6 +1816,14 @@ BOARDS = { "name": "oddWires IoT-Bus Proteus", "variant": VARIANT_ESP32, }, + "ioxesp32": { + "name": "ArtronShop IOXESP32", + "variant": VARIANT_ESP32, + }, + "ioxesp32ps": { + "name": "ArtronShop IOXESP32PS", + "variant": VARIANT_ESP32, + }, "kb32-ft": { "name": "MakerAsia KB32-FT", "variant": VARIANT_ESP32, @@ -1720,10 +1836,26 @@ BOARDS = { "name": "Labplus mPython", "variant": VARIANT_ESP32, }, + "lilka_v2": { + "name": "Lilka v2", + "variant": VARIANT_ESP32S3, + }, + "lilygo-t-display": { + "name": "LilyGo T-Display", + "variant": VARIANT_ESP32, + }, + "lilygo-t-display-s3": { + "name": "LilyGo T-Display-S3", + "variant": VARIANT_ESP32S3, + }, "lionbit": { "name": "Lion:Bit Dev Board", "variant": VARIANT_ESP32, }, + "lionbits3": { + "name": "Lion:Bit S3 STEM Dev Board", + "variant": VARIANT_ESP32S3, + }, "lolin32_lite": { "name": "WEMOS LOLIN32 Lite", "variant": VARIANT_ESP32, @@ -1752,10 +1884,18 @@ BOARDS = { "name": "WEMOS LOLIN S2 PICO", "variant": VARIANT_ESP32S2, }, + "lolin_s3_mini": { + "name": "WEMOS LOLIN S3 Mini", + "variant": VARIANT_ESP32S3, + }, "lolin_s3": { "name": "WEMOS LOLIN S3", "variant": VARIANT_ESP32S3, }, + "lolin_s3_pro": { + "name": "WEMOS LOLIN S3 PRO", + "variant": VARIANT_ESP32S3, + }, "lopy4": { "name": "Pycom LoPy4", "variant": VARIANT_ESP32, @@ -1768,10 +1908,18 @@ BOARDS = { "name": "M5Stack-ATOM", "variant": VARIANT_ESP32, }, + "m5stack-atoms3": { + "name": "M5Stack AtomS3", + "variant": VARIANT_ESP32S3, + }, "m5stack-core2": { "name": "M5Stack Core2", "variant": VARIANT_ESP32, }, + "m5stack-core-esp32-16M": { + "name": "M5Stack Core ESP32 16M", + "variant": VARIANT_ESP32, + }, "m5stack-core-esp32": { "name": "M5Stack Core ESP32", "variant": VARIANT_ESP32, @@ -1780,6 +1928,10 @@ BOARDS = { "name": "M5Stack-Core Ink", "variant": VARIANT_ESP32, }, + "m5stack-cores3": { + "name": "M5Stack CoreS3", + "variant": VARIANT_ESP32S3, + }, "m5stack-fire": { "name": "M5Stack FIRE", "variant": VARIANT_ESP32, @@ -1788,6 +1940,14 @@ BOARDS = { "name": "M5Stack GREY ESP32", "variant": VARIANT_ESP32, }, + "m5stack_paper": { + "name": "M5Stack Paper", + "variant": VARIANT_ESP32, + }, + "m5stack-stamps3": { + "name": "M5Stack StampS3", + "variant": VARIANT_ESP32S3, + }, "m5stack-station": { "name": "M5Stack Station", "variant": VARIANT_ESP32, @@ -1796,6 +1956,10 @@ BOARDS = { "name": "M5Stack Timer CAM", "variant": VARIANT_ESP32, }, + "m5stamp-pico": { + "name": "M5Stamp-Pico", + "variant": VARIANT_ESP32, + }, "m5stick-c": { "name": "M5Stick-C", "variant": VARIANT_ESP32, @@ -1832,10 +1996,26 @@ BOARDS = { "name": "Deparment of Alchemy MiniMain ESP32-S2", "variant": VARIANT_ESP32S2, }, + "motorgo_mini_1": { + "name": "MotorGo Mini 1 (ESP32-S3)", + "variant": VARIANT_ESP32S3, + }, + "namino_arancio": { + "name": "Namino Arancio", + "variant": VARIANT_ESP32S3, + }, + "namino_rosso": { + "name": "Namino Rosso", + "variant": VARIANT_ESP32S3, + }, "nano32": { "name": "MakerAsia Nano32", "variant": VARIANT_ESP32, }, + "nebulas3": { + "name": "Kinetic Dynamics Nebula S3", + "variant": VARIANT_ESP32S3, + }, "nina_w10": { "name": "u-blox NINA-W10 series", "variant": VARIANT_ESP32, @@ -1896,10 +2076,22 @@ BOARDS = { "name": "Munich Labs RedPill ESP32-S3", "variant": VARIANT_ESP32S3, }, + "roboheart_hercules": { + "name": "RoboHeart Hercules", + "variant": VARIANT_ESP32, + }, "seeed_xiao_esp32c3": { "name": "Seeed Studio XIAO ESP32C3", "variant": VARIANT_ESP32C3, }, + "seeed_xiao_esp32s3": { + "name": "Seeed Studio XIAO ESP32S3", + "variant": VARIANT_ESP32S3, + }, + "sensebox_mcu_esp32s2": { + "name": "senseBox MCU-S2 ESP32-S2", + "variant": VARIANT_ESP32S2, + }, "sensesiot_weizen": { "name": "LOGISENSES Senses Weizen", "variant": VARIANT_ESP32, @@ -1912,6 +2104,10 @@ BOARDS = { "name": "S.ODI Ultra v1", "variant": VARIANT_ESP32, }, + "sparkfun_esp32c6_thing_plus": { + "name": "Sparkfun ESP32-C6 Thing Plus", + "variant": VARIANT_ESP32C6, + }, "sparkfun_esp32_iot_redboard": { "name": "SparkFun ESP32 IoT RedBoard", "variant": VARIANT_ESP32, @@ -2004,6 +2200,10 @@ BOARDS = { "name": "Unexpected Maker FeatherS3", "variant": VARIANT_ESP32S3, }, + "um_nanos3": { + "name": "Unexpected Maker NanoS3", + "variant": VARIANT_ESP32S3, + }, "um_pros3": { "name": "Unexpected Maker PROS3", "variant": VARIANT_ESP32S3, @@ -2040,6 +2240,14 @@ BOARDS = { "name": "uPesy ESP32 Wrover DevKit", "variant": VARIANT_ESP32, }, + "valtrack_v4_mfw_esp32_c3": { + "name": "Valetron Systems VALTRACK-V4MVF", + "variant": VARIANT_ESP32C3, + }, + "valtrack_v4_vts_esp32_c3": { + "name": "Valetron Systems VALTRACK-V4VTS", + "variant": VARIANT_ESP32C3, + }, "vintlabs-devkit-v1": { "name": "VintLabs ESP32 Devkit", "variant": VARIANT_ESP32, diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 48c8b2b04d..ff8e663ec1 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } #else uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } #endif -uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); } +uint32_t arch_get_cpu_freq_hz() { + rtc_cpu_freq_config_t config; + rtc_clk_cpu_freq_get_config(&config); + return config.freq_mhz * 1000000U; +} #ifdef USE_ESP_IDF TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d7dcf93f86..b10e454c21 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,6 +27,9 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; +static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); + void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Setting up BLE..."); @@ -322,7 +325,8 @@ void ESP32BLE::loop() { default: break; } - delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) + ble_event->~BLEEvent(); + EVENT_ALLOCATOR.deallocate(ble_event, 1); ble_event = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { @@ -331,9 +335,14 @@ void ESP32BLE::loop() { } void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); @@ -344,9 +353,14 @@ void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, gatts_if, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, gatts_if, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { @@ -358,9 +372,14 @@ void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, gattc_if, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 92b7c60368..1d340c76d9 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -83,7 +83,7 @@ esp_err_t BLEAdvertising::services_advertisement_() { esp_err_t err; this->advertising_data_.set_scan_rsp = false; - this->advertising_data_.include_name = true; + this->advertising_data_.include_name = !this->scan_response_; this->advertising_data_.include_txpower = !this->scan_response_; err = esp_ble_gap_config_adv_data(&this->advertising_data_); if (err != ESP_OK) { diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 5b31b97ae2..c98477e121 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -26,10 +26,10 @@ template class Queue { void push(T *element) { if (element == nullptr) return; - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - q_.push(element); - xSemaphoreGive(m_); - } + // It is not called from main loop. Thus it won't block main thread. + xSemaphoreTake(m_, portMAX_DELAY); + q_.push(element); + xSemaphoreGive(m_); } T *pop() { diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 98e7792792..53c430350c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -44,6 +44,50 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } +void BLEClientBase::dump_config() { + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_)); + std::string state_name; + switch (this->state()) { + case espbt::ClientState::INIT: + state_name = "INIT"; + break; + case espbt::ClientState::DISCONNECTING: + state_name = "DISCONNECTING"; + break; + case espbt::ClientState::IDLE: + state_name = "IDLE"; + break; + case espbt::ClientState::SEARCHING: + state_name = "SEARCHING"; + break; + case espbt::ClientState::DISCOVERED: + state_name = "DISCOVERED"; + break; + case espbt::ClientState::READY_TO_CONNECT: + state_name = "READY_TO_CONNECT"; + break; + case espbt::ClientState::CONNECTING: + state_name = "CONNECTING"; + break; + case espbt::ClientState::CONNECTED: + state_name = "CONNECTED"; + break; + case espbt::ClientState::ESTABLISHED: + state_name = "ESTABLISHED"; + break; + default: + state_name = "UNKNOWN_STATE"; + break; + } + ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str()); + if (this->status_ == ESP_GATT_NO_RESOURCES) { + ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); + } else if (this->status_ != ESP_GATT_OK) { + ESP_LOGW(TAG, " Failed due to error code %d", this->status_); + } +} + bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { if (!this->auto_connect_) return false; @@ -129,6 +173,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } else { ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, this->address_str_.c_str(), param->reg.app_id, param->reg.status); + this->status_ = param->reg.status; + this->mark_failed(); } break; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index fca66c0b3c..84c35c4633 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -26,6 +26,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void setup() override; void loop() override; float get_setup_priority() const override; + void dump_config() override; void run_later(std::function &&f); // NOLINT bool parse_device(const espbt::ESPBTDevice &device) override; @@ -103,6 +104,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { bool paired_{false}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; std::vector services_; + esp_gatt_status_t status_{ESP_GATT_OK}; void log_event_(const char *name); }; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 6d051e3d4a..5fff9dbcad 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -58,7 +58,6 @@ void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); this->scan_end_lock_ = xSemaphoreCreateMutex(); - this->scanner_idle_ = true; #ifdef USE_OTA ota::get_global_ota_callback()->add_on_state_callback( @@ -107,6 +106,15 @@ void ESP32BLETracker::loop() { break; } } + if (connecting != connecting_ || discovered != discovered_ || searching != searching_ || + disconnecting != disconnecting_) { + connecting_ = connecting; + discovered_ = discovered; + searching_ = searching; + disconnecting_ = disconnecting; + ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, + searching_, disconnecting_); + } bool promote_to_connecting = discovered && !searching && !connecting; if (!this->scanner_idle_) { @@ -183,8 +191,9 @@ void ESP32BLETracker::loop() { } if (this->scan_start_failed_ || this->scan_set_param_failed_) { - if (this->scan_start_fail_count_ == 255) { - ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after 255 attempts, rebooting to restore BLE stack..."); + if (this->scan_start_fail_count_ == std::numeric_limits::max()) { + ESP_LOGE(TAG, "ESP-IDF BLE scan could not restart after %d attempts, rebooting to restore BLE stack...", + std::numeric_limits::max()); App.reboot(); } if (xSemaphoreTake(this->scan_end_lock_, 0L)) { @@ -282,6 +291,12 @@ void ESP32BLETracker::start_scan_(bool first) { this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_window = this->scan_window_; + // Start timeout before scan is started. Otherwise scan never starts if any error. + this->set_timeout("scan", this->scan_duration_ * 2000, []() { + ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); + App.reboot(); + }); + esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_scan_params failed: %d", err); @@ -293,11 +308,6 @@ void ESP32BLETracker::start_scan_(bool first) { return; } this->scanner_idle_ = false; - - this->set_timeout("scan", this->scan_duration_ * 2000, []() { - ESP_LOGE(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); - App.reboot(); - }); } void ESP32BLETracker::end_of_scan_() { @@ -371,6 +381,7 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { + ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); if (param.status == ESP_BT_STATUS_DONE) { this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; } else { @@ -379,20 +390,25 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t: } void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { + ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); this->scan_start_failed_ = param.status; if (param.status == ESP_BT_STATUS_SUCCESS) { this->scan_start_fail_count_ = 0; } else { - this->scan_start_fail_count_++; + if (this->scan_start_fail_count_ != std::numeric_limits::max()) { + this->scan_start_fail_count_++; + } xSemaphoreGive(this->scan_end_lock_); } } void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); xSemaphoreGive(this->scan_end_lock_); } void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { @@ -663,7 +679,14 @@ void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, " Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Window: %.1f ms", this->scan_window_ * 0.625f); ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); - ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False"); + ESP_LOGCONFIG(TAG, " Continuous Scanning: %s", YESNO(this->scan_continuous_)); + ESP_LOGCONFIG(TAG, " Scanner Idle: %s", YESNO(this->scanner_idle_)); + ESP_LOGCONFIG(TAG, " Scan End: %s", YESNO(xSemaphoreGetMutexHolder(this->scan_end_lock_) == nullptr)); + ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, + searching_, disconnecting_); + if (this->scan_start_fail_count_) { + ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); + } } void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 2fc5da829d..52b091619e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -178,7 +178,7 @@ class ESPBTClient : public ESPBTDeviceListener { int app_id; protected: - ClientState state_; + ClientState state_{ClientState::INIT}; }; class ESP32BLETracker : public Component, @@ -229,7 +229,7 @@ class ESP32BLETracker : public Component, /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); - int app_id_; + int app_id_{0}; /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -242,10 +242,10 @@ class ESP32BLETracker : public Component, uint32_t scan_duration_; uint32_t scan_interval_; uint32_t scan_window_; - uint8_t scan_start_fail_count_; + uint8_t scan_start_fail_count_{0}; bool scan_continuous_; bool scan_active_; - bool scanner_idle_; + bool scanner_idle_{true}; bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; @@ -260,6 +260,10 @@ class ESP32BLETracker : public Component, esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + int connecting_{0}; + int discovered_{0}; + int searching_{0}; + int disconnecting_{0}; }; // NOLINTNEXTLINE diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d36b50feb0..c67431077c 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() { this->set_state_(improv::STATE_AUTHORIZED); } else #else - this->set_state_(improv::STATE_AUTHORIZED); + { this->set_state_(improv::STATE_AUTHORIZED); } #endif { if (!this->check_identify_()) diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index bda240680b..171c335727 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,7 +1,8 @@ -import esphome.config_validation as cv import esphome.codegen as cg - from esphome.components import esp32 +import esphome.config_validation as cv +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] @@ -36,8 +37,32 @@ RMT_CHANNEL_ENUMS = { } -def validate_rmt_channel(*, tx: bool): +def use_new_rmt_driver(): + framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): + return True + return False + +def validate_clock_resolution(): + def _validator(value): + cv.only_on_esp32(value) + value = cv.int_(value) + variant = esp32.get_esp32_variant() + if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000: + raise cv.Invalid( + f"ESP32 variant {variant} has a max clock_resolution of 32000000." + ) + if value > 80000000: + raise cv.Invalid( + f"ESP32 variant {variant} has a max clock_resolution of 80000000." + ) + return value + + return _validator + + +def validate_rmt_channel(*, tx: bool): rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index c2209f7a6c..4e8c862c23 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -1,5 +1,5 @@ -#include #include "led_strip.h" +#include #ifdef USE_ESP32 @@ -13,9 +13,13 @@ namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; +#ifdef USE_ESP32_VARIANT_ESP32H2 +static const uint32_t RMT_CLK_FREQ = 32000000; +static const uint8_t RMT_CLK_DIV = 1; +#else static const uint32_t RMT_CLK_FREQ = 80000000; - static const uint8_t RMT_CLK_DIV = 2; +#endif void ESP32RMTLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip..."); @@ -29,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } + memset(this->buf_, 0, buffer_size); this->effect_data_ = allocator.allocate(this->num_leds_); if (this->effect_data_ == nullptr) { @@ -37,9 +42,48 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } +#if ESP_IDF_VERSION_MAJOR >= 5 + RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); + + // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); + + rmt_tx_channel_config_t channel; + memset(&channel, 0, sizeof(channel)); + channel.clk_src = RMT_CLK_SRC_DEFAULT; + channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV; + channel.gpio_num = gpio_num_t(this->pin_); + channel.mem_block_symbols = this->rmt_symbols_; + channel.trans_queue_depth = 1; + channel.flags.io_loop_back = 0; + channel.flags.io_od_mode = 0; + channel.flags.invert_out = 0; + channel.flags.with_dma = 0; + channel.intr_priority = 0; + if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) { + ESP_LOGE(TAG, "Channel creation failed"); + this->mark_failed(); + return; + } + + rmt_copy_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) { + ESP_LOGE(TAG, "Encoder creation failed"); + this->mark_failed(); + return; + } + + if (rmt_enable(this->channel_) != ESP_OK) { + ESP_LOGE(TAG, "Enabling channel failed"); + this->mark_failed(); + return; + } +#else RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); - this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + - 1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset + + // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); rmt_config_t config; memset(&config, 0, sizeof(config)); @@ -64,6 +108,7 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } +#endif } void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, @@ -100,7 +145,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { ESP_LOGVV(TAG, "Writing RGB values to bus..."); - if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) { +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000); +#else + esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)); +#endif + if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX timeout"); this->status_set_warning(); return; @@ -112,7 +162,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { size_t size = 0; size_t len = 0; uint8_t *psrc = this->buf_; +#if ESP_IDF_VERSION_MAJOR >= 5 + rmt_symbol_word_t *pdest = this->rmt_buf_; +#else rmt_item32_t *pdest = this->rmt_buf_; +#endif while (size < buffer_size) { uint8_t b = *psrc; for (int i = 0; i < 8; i++) { @@ -130,7 +184,16 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { len++; } - if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { +#if ESP_IDF_VERSION_MAJOR >= 5 + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.loop_count = 0; + config.flags.eot_level = 0; + error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); +#else + error = rmt_write_items(this->channel_, this->rmt_buf_, len, false); +#endif + if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); this->status_set_warning(); return; @@ -186,7 +249,11 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index void ESP32RMTLEDStripLightOutput::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:"); ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); +#if ESP_IDF_VERSION_MAJOR >= 5 + ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_); +#else ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); +#endif const char *rgb_order; switch (this->rgb_order_) { case ORDER_RGB: diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index d21bd86e75..fe49b9a2f3 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -9,8 +9,14 @@ #include "esphome/core/helpers.h" #include -#include #include +#include + +#if ESP_IDF_VERSION_MAJOR >= 5 +#include +#else +#include +#endif namespace esphome { namespace esp32_rmt_led_strip { @@ -54,7 +60,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint32_t reset_time_high, uint32_t reset_time_low); void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } +#if ESP_IDF_VERSION_MAJOR >= 5 + void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } +#else void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } +#endif void clear_effect_data() override { for (int i = 0; i < this->size(); i++) @@ -70,7 +80,17 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; +#if ESP_IDF_VERSION_MAJOR >= 5 + rmt_channel_handle_t channel_{nullptr}; + rmt_encoder_handle_t encoder_{nullptr}; + rmt_symbol_word_t *rmt_buf_{nullptr}; + rmt_symbol_word_t bit0_, bit1_, reset_; + uint32_t rmt_symbols_; +#else rmt_item32_t *rmt_buf_{nullptr}; + rmt_item32_t bit0_, bit1_, reset_; + rmt_channel_t channel_{RMT_CHANNEL_0}; +#endif uint8_t pin_; uint16_t num_leds_; @@ -78,9 +98,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { bool is_wrgb_; bool use_psram_; - rmt_item32_t bit0_, bit1_, reset_; RGBOrder rgb_order_; - rmt_channel_t channel_; uint32_t last_refresh_{0}; optional max_refresh_rate_{}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 976f70e858..64104bb6de 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import logging from esphome import pins import esphome.codegen as cg @@ -13,7 +14,11 @@ from esphome.const import ( CONF_PIN, CONF_RGB_ORDER, CONF_RMT_CHANNEL, + CONF_RMT_SYMBOLS, ) +from esphome.core import CORE + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -23,8 +28,6 @@ ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( "ESP32RMTLEDStripLightOutput", light.AddressableLight ) -rmt_channel_t = cg.global_ns.enum("rmt_channel_t") - RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") RGB_ORDERS = { @@ -65,6 +68,53 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" +class OptionalForIDF5(cv.SplitDefault): + @property + def default(self): + if not esp32_rmt.use_new_rmt_driver(): + return cv.UNDEFINED + return super().default + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +def only_with_new_rmt_driver(obj): + if not esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is only available for the IDF framework version 5." + ) + return obj + + +def not_with_new_rmt_driver(obj): + if esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is not available for the IDF framework version 5." + ) + return obj + + +def final_validation(config): + if not esp32_rmt.use_new_rmt_driver(): + if CONF_RMT_CHANNEL not in config: + if CORE.using_esp_idf: + raise cv.Invalid( + "rmt_channel is a required option for IDF version < 5." + ) + raise cv.Invalid( + "rmt_channel is a required option for the Arduino framework." + ) + _LOGGER.warning( + "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." + ) + + +FINAL_VALIDATE_SCHEMA = final_validation + + CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { @@ -72,7 +122,18 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), - cv.Required(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True), + cv.Optional(CONF_RMT_CHANNEL): cv.All( + not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) + ), + OptionalForIDF5( + CONF_RMT_SYMBOLS, + esp32_idf=64, + esp32_s2_idf=64, + esp32_s3_idf=48, + esp32_c3_idf=48, + esp32_c6_idf=48, + esp32_h2_idf=48, + ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, @@ -148,8 +209,12 @@ async def to_code(config): cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) - cg.add( - var.set_rmt_channel( - getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") + if esp32_rmt.use_new_rmt_driver(): + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + else: + rmt_channel_t = cg.global_ns.enum("rmt_channel_t") + cg.add( + var.set_rmt_channel( + getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") + ) ) - ) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index dca37b8dc2..ab760a9b6c 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -94,11 +94,11 @@ CLK_MODES = { MANUAL_IP_SCHEMA = cv.Schema( { - cv.Required(CONF_STATIC_IP): cv.ipv4, - cv.Required(CONF_GATEWAY): cv.ipv4, - cv.Required(CONF_SUBNET): cv.ipv4, - cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, - cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, + cv.Required(CONF_STATIC_IP): cv.ipv4address, + cv.Required(CONF_GATEWAY): cv.ipv4address, + cv.Required(CONF_SUBNET): cv.ipv4address, + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address, } ) @@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate def manual_ip(config): return cg.StructInitializer( ManualIP, - ("static_ip", IPAddress(*config[CONF_STATIC_IP].args)), - ("gateway", IPAddress(*config[CONF_GATEWAY].args)), - ("subnet", IPAddress(*config[CONF_SUBNET].args)), - ("dns1", IPAddress(*config[CONF_DNS1].args)), - ("dns2", IPAddress(*config[CONF_DNS2].args)), + ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), + ("gateway", IPAddress(str(config[CONF_GATEWAY]))), + ("subnet", IPAddress(str(config[CONF_SUBNET]))), + ("dns1", IPAddress(str(config[CONF_DNS1]))), + ("dns2", IPAddress(str(config[CONF_DNS2]))), ) 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/font/__init__.py b/esphome/components/font/__init__.py index c397ba8306..e2051298fe 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -51,8 +51,11 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs" # Cache loaded freetype fonts class FontCache(dict): def __missing__(self, key): - res = self[key] = freetype.Face(key) - return res + try: + res = self[key] = freetype.Face(key) + return res + except freetype.FT_Exception as e: + raise cv.Invalid(f"Could not load Font file {key}: {e}") from e FONT_CACHE = FontCache() diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index aeca0f5cc0..8c4cba34b3 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -133,9 +133,11 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo auto diff_r = (float) color.r - (float) background.r; auto diff_g = (float) color.g - (float) background.g; auto diff_b = (float) color.b - (float) background.b; + auto diff_w = (float) color.w - (float) background.w; auto b_r = (float) background.r; auto b_g = (float) background.g; - auto b_b = (float) background.g; + auto b_b = (float) background.b; + auto b_w = (float) background.w; for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { uint8_t pixel = 0; @@ -153,8 +155,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo display->draw_pixel_at(glyph_x, glyph_y, color); } else if (pixel != 0) { auto on = (float) pixel / (float) bpp_max; - auto blended = - Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b)); + auto blended = Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), + (uint8_t) (diff_b * on + b_b), (uint8_t) (diff_w * on + b_w)); display->draw_pixel_at(glyph_x, glyph_y, blended); } } diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index 7f980ca0ad..b1db58654b 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() { if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); return; - } else + } else { ESP_LOGVV(TAG, "Good packet found."); + } this->have_good_data_ = true; uint8_t status = this->rx_message_[29]; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index ba80c1ca1b..f8c0a7587e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -342,8 +342,9 @@ bool HaierClimateBase::prepare_pending_action() { this->action_request_.reset(); return false; } - } else + } else { return false; + } } ClimateTraits HaierClimateBase::traits() { return traits_; } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index c95a87223d..9b59dd0c10 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -710,8 +710,9 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo alarm_code++; } active_alarms_[i] = packet[2 + i]; - } else + } else { alarm_code += 8; + } } } else { float alarm_count = 0.0f; diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 144dcc9bfa..55f0599cba 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -87,8 +87,9 @@ void HeatpumpIRClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } } void HeatpumpIRClimate::transmit_state() { 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/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 3a9c229778..ac3a754024 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -17,14 +17,14 @@ void IDFI2CBus::setup() { ESP_LOGCONFIG(TAG, "Setting up I2C bus..."); static i2c_port_t next_port = I2C_NUM_0; port_ = next_port; -#if I2C_NUM_MAX > 1 +#if SOC_I2C_NUM > 1 next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; #else next_port = I2C_NUM_MAX; #endif if (port_ == I2C_NUM_MAX) { - ESP_LOGE(TAG, "Too many I2C buses configured"); + ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_I2C_NUM); this->mark_failed(); return; } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 23689afb91..4dbc9dcdac 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -25,11 +25,13 @@ void I2SAudioMicrophone::setup() { } } else #endif - if (this->pdm_) { - if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "PDM only works on I2S0!"); - this->mark_failed(); - return; + { + if (this->pdm_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "PDM only works on I2S0!"); + this->mark_failed(); + return; + } } } } diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index d2a582c2cc..46f1b00d05 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -247,7 +247,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { // Ensure ring buffer is at least as large as the total size of the DMA buffers const size_t ring_buffer_size = - std::min((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms); + std::max((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms); if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) { // Failed to allocate buffers 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 4669a3418a..a503e8f471 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -6,7 +6,7 @@ import logging from pathlib import Path import re -import puremagic +from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg @@ -29,21 +29,259 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] -MULTI_CONF = True -MULTI_CONF_NO_DEFAULT = True image_ns = cg.esphome_ns.namespace("image") ImageType = image_ns.enum("ImageType") + +CONF_OPAQUE = "opaque" +CONF_CHROMA_KEY = "chroma_key" +CONF_ALPHA_CHANNEL = "alpha_channel" +CONF_INVERT_ALPHA = "invert_alpha" + +TRANSPARENCY_TYPES = ( + CONF_OPAQUE, + CONF_CHROMA_KEY, + CONF_ALPHA_CHANNEL, +) + + +def get_image_type_enum(type): + return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}") + + +def get_transparency_enum(transparency): + return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}") + + +class ImageEncoder: + """ + Superclass of image type encoders + """ + + # Control which transparency options are available for a given type + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + # All imageencoder types are valid + @staticmethod + def validate(value): + return value + + def __init__(self, width, height, transparency, dither, invert_alpha): + """ + :param width: The image width in pixels + :param height: The image height in pixels + :param transparency: Transparency type + :param dither: Dither method + :param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours. + """ + self.transparency = transparency + self.width = width + self.height = height + self.data = [0 for _ in range(width * height)] + self.dither = dither + self.index = 0 + self.invert_alpha = invert_alpha + self.path = "" + + 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 + + def encode(self, pixel): + """ + Encode a single pixel + """ + + def end_row(self): + """ + Marks the end of a pixel row + :return: + """ + + +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} + + def __init__(self, width, height, transparency, dither, invert_alpha): + self.width8 = (width + 7) // 8 + super().__init__(self.width8, height, transparency, dither, invert_alpha) + self.bitno = 0 + + 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): + if self.invert_alpha: + pixel = not pixel + if pixel: + self.data[self.index] |= 0x80 >> (self.bitno % 8) + self.bitno += 1 + if self.bitno == 8: + self.bitno = 0 + self.index += 1 + + def end_row(self): + """ + Pad rows to a byte boundary + """ + if self.bitno != 0: + self.bitno = 0 + self.index += 1 + + +class ImageGrayscale(ImageEncoder): + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} + + 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): + b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if b == 1: + b = 0 + if a != 0xFF: + b = 1 + if self.invert_alpha: + b ^= 0xFF + if self.transparency == CONF_ALPHA_CHANNEL: + if a != 0xFF: + b = a + self.data[self.index] = b + self.index += 1 + + +class ImageRGB565(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image, path): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + r = r >> 3 + g = g >> 2 + b = b >> 3 + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + rgb = (r << 11) | (g << 5) | b + self.data[self.index] = rgb >> 8 + self.index += 1 + self.data[self.index] = rgb & 0xFF + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ImageRGB(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image, path): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + self.data[self.index] = r + self.index += 1 + self.data[self.index] = g + self.index += 1 + self.data[self.index] = b + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ReplaceWith: + """ + Placeholder class to provide feedback on deprecated features + """ + + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + def __init__(self, replace_with): + self.replace_with = replace_with + + def validate(self, value): + raise cv.Invalid( + f"Image type {value} is removed; replace with {self.replace_with}" + ) + + IMAGE_TYPE = { - "BINARY": ImageType.IMAGE_TYPE_BINARY, - "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, - "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, - "RGB565": ImageType.IMAGE_TYPE_RGB565, - "RGB24": ImageType.IMAGE_TYPE_RGB24, - "RGBA": ImageType.IMAGE_TYPE_RGBA, + "BINARY": ImageBinary, + "GRAYSCALE": ImageGrayscale, + "RGB565": ImageRGB565, + "RGB": ImageRGB, + "TRANSPARENT_BINARY": ReplaceWith( + "'type: BINARY' and 'use_transparency: chroma_key'" + ), + "RGB24": ReplaceWith("'type: RGB'"), + "RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), } +TransparencyType = image_ns.enum("TransparencyType") + CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. @@ -53,17 +291,11 @@ SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" SOURCE_WEB = "web" - Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value: dict) -> Path: - base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" - return base_dir / f"{value[CONF_ICON]}.svg" - - -def compute_local_image_path(value: dict) -> Path: - url = value[CONF_URL] +def compute_local_image_path(value) -> Path: + url = value[CONF_URL] if isinstance(value, dict) else value h = hashlib.new("sha256") h.update(url.encode()) key = h.hexdigest()[:8] @@ -71,30 +303,38 @@ def compute_local_image_path(value: dict) -> Path: return base_dir / key -def download_mdi(value): - validate_cairosvg_installed(value) +def local_path(value): + value = value[CONF_PATH] if isinstance(value, dict) else value + return str(CORE.relative_config_path(value)) - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) + +def download_file(url, path): + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + return str(path) + + +def download_mdi(value): + mdi_id = value[CONF_ICON] if isinstance(value, dict) else value + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" + path = base_dir / f"{mdi_id}.svg" url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + return download_file(url, path) def download_image(value): - url = value[CONF_URL] - path = compute_local_image_path(value) - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + value = value[CONF_URL] if isinstance(value, dict) else value + return download_file(value, compute_local_image_path(value)) -def validate_cairosvg_installed(value): - """Validate that cairosvg is installed""" +def is_svg_file(file): + if not file: + return False + with open(file, "rb") as f: + return " 500 or height > 500): + if not resize and (width > 500 or height > 500): _LOGGER.warning( 'The image "%s" you requested is very big. Please consider' " using the resize parameter.", path, ) - transparent = config[CONF_USE_TRANSPARENCY] - dither = ( Image.Dither.NONE if config[CONF_DITHER] == "NONE" else Image.Dither.FLOYDSTEINBERG ) - if config[CONF_TYPE] == "GRAYSCALE": - image = image.convert("LA", dither=dither) - pixels = list(image.getdata()) - data = [0 for _ in range(height * width)] - pos = 0 - for g, a in pixels: - if transparent: - if g == 1: - g = 0 - if a < 0x80: - g = 1 + type = config[CONF_TYPE] + transparency = config[CONF_USE_TRANSPARENCY] + invert_alpha = config[CONF_INVERT_ALPHA] + frame_count = 1 + if all_frames: + try: + frame_count = image.n_frames + except AttributeError: + pass + if frame_count <= 1: + _LOGGER.warning("Image file %s has no animation frames", path) - data[pos] = g - pos += 1 + total_rows = height * frame_count + encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) + for frame_index in range(frame_count): + image.seek(frame_index) + pixels = encoder.convert(image.resize((width, height)), path).getdata() + for row in range(height): + for col in range(width): + encoder.encode(pixels[row * width + col]) + encoder.end_row() - elif config[CONF_TYPE] == "RGBA": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 4)] - pos = 0 - for r, g, b, a in pixels: - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 3)] - pos = 0 - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565"]: - image = image.convert("RGBA") - pixels = list(image.getdata()) - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel)] - pos = 0 - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) - image = image.convert("1", dither=dither) - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if transparent and has_alpha: - a = alpha.getpixel((x, y)) - if not a: - continue - elif image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] + rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - var = cg.new_Pvariable( - config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] - ) - cg.add(var.set_transparency(transparent)) + image_type = get_image_type_enum(type) + trans_value = get_transparency_enum(encoder.transparency) + + return prog_arr, width, height, image_type, trans_value, frame_count + + +async def to_code(config): + if isinstance(config, list): + for entry in config: + await to_code(entry) + elif CONF_ID not in config: + for entry in config.values(): + await to_code(entry) + else: + prog_arr, width, height, image_type, trans_value, _ = await write_image(config) + cg.new_Pvariable( + config[CONF_ID], prog_arr, width, height, image_type, trans_value + ) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index ca2f659fb0..f05f4af711 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color for (int img_y = 0; img_y < height_; img_y++) { if (this->get_binary_pixel_(img_x, img_y)) { display->draw_pixel_at(x + img_x, y + img_y, color_on); - } else if (!this->transparent_) { + } else if (!this->transparency_) { display->draw_pixel_at(x + img_x, y + img_y, color_off); } } @@ -22,10 +22,27 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color case IMAGE_TYPE_GRAYSCALE: for (int img_x = 0; img_x < width_; img_x++) { for (int img_y = 0; img_y < height_; img_y++) { - auto color = this->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; @@ -39,20 +56,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color } } break; - case IMAGE_TYPE_RGB24: + case IMAGE_TYPE_RGB: for (int img_x = 0; img_x < width_; img_x++) { for (int img_y = 0; img_y < height_; img_y++) { - auto color = this->get_rgb24_pixel_(img_x, img_y); - if (color.w >= 0x80) { - display->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGBA: - for (int img_x = 0; img_x < width_; img_x++) { - for (int img_y = 0; img_y < height_; img_y++) { - auto color = this->get_rgba_pixel_(img_x, img_y); + auto color = this->get_rgb_pixel_(img_x, img_y); if (color.w >= 0x80) { display->draw_pixel_at(x + img_x, y + img_y, color); } @@ -61,20 +68,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color break; } } -Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { +Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return color_off; switch (this->type_) { case IMAGE_TYPE_BINARY: - return this->get_binary_pixel_(x, y) ? color_on : color_off; + if (this->get_binary_pixel_(x, y)) + return color_on; + return color_off; case IMAGE_TYPE_GRAYSCALE: return this->get_grayscale_pixel_(x, y); case IMAGE_TYPE_RGB565: return this->get_rgb565_pixel_(x, y); - case IMAGE_TYPE_RGB24: - return this->get_rgb24_pixel_(x, y); - case IMAGE_TYPE_RGBA: - return this->get_rgba_pixel_(x, y); + case IMAGE_TYPE_RGB: + return this->get_rgb_pixel_(x, y); default: return color_off; } @@ -98,23 +105,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; break; - case IMAGE_TYPE_RGB24: - this->dsc_.header.cf = LV_IMG_CF_RGB888; + case IMAGE_TYPE_RGB: +#if LV_COLOR_DEPTH == 32 + switch (this->transparent_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } +#else + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888; +#endif break; case IMAGE_TYPE_RGB565: #if LV_COLOR_DEPTH == 16 - this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } #else - this->dsc_.header.cf = LV_IMG_CF_RGB565; -#endif - break; - - case IMAGE_TYPE_RGBA: -#if LV_COLOR_DEPTH == 32 - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; -#else - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } @@ -128,51 +152,81 @@ bool Image::get_binary_pixel_(int x, int y) const { const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -Color Image::get_rgba_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Image::get_rgb24_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 3; +Color Image::get_rgb_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8; Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; + progmem_read_byte(this->data_start_ + pos + 2), 0xFF); + + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (color.g == 1 && color.r == 0 && color.b == 0) { + // (0, 1, 0) has been defined as transparent color for non-alpha images. + color.w = 0; + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: + color.w = progmem_read_byte(this->data_start_ + (pos + 3)); + break; + default: + break; } return color; } Color Image::get_rgb565_pixel_(int x, int y) const { - const uint8_t *pos = this->data_start_; - if (this->transparent_) { - pos += (x + y * this->width_) * 3; - } else { - pos += (x + y * this->width_) * 2; - } + const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8; uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); - return color; + auto a = 0xFF; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + a = progmem_read_byte(pos + 2); + break; + case TRANSPARENCY_CHROMA_KEY: + if (rgb565 == 0x0020) + a = 0; + break; + default: + break; + } + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); } 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 && transparent_) ? 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_; } ImageType Image::get_type() const { return this->type_; } -Image::Image(const uint8_t *data_start, int width, int height, ImageType type) - : width_(width), height_(height), type_(type), data_start_(data_start) {} +Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency) + : width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) { + switch (this->type_) { + case IMAGE_TYPE_BINARY: + this->bpp_ = 1; + break; + case IMAGE_TYPE_GRAYSCALE: + this->bpp_ = 8; + break; + case IMAGE_TYPE_RGB565: + this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16; + break; + case IMAGE_TYPE_RGB: + this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24; + break; + } +} } // namespace image } // namespace esphome diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 40370d18da..4024ab1357 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -12,51 +12,40 @@ namespace image { enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, - IMAGE_TYPE_RGB24 = 2, + IMAGE_TYPE_RGB = 2, IMAGE_TYPE_RGB565 = 3, - IMAGE_TYPE_RGBA = 4, +}; + +enum Transparency { + TRANSPARENCY_OPAQUE = 0, + TRANSPARENCY_CHROMA_KEY = 1, + TRANSPARENCY_ALPHA_CHANNEL = 2, }; class Image : public display::BaseImage { public: - Image(const uint8_t *data_start, int width, int height, ImageType type); + Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency); Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; const uint8_t *get_data_start() const { return this->data_start_; } ImageType get_type() const; - int get_bpp() const { - switch (this->type_) { - case IMAGE_TYPE_BINARY: - return 1; - case IMAGE_TYPE_GRAYSCALE: - return 8; - case IMAGE_TYPE_RGB565: - return this->transparent_ ? 24 : 16; - case IMAGE_TYPE_RGB24: - return 24; - case IMAGE_TYPE_RGBA: - return 32; - } - return 0; - } + int get_bpp() const { return this->bpp_; } /// Return the stride of the image in bytes, that is, the distance in bytes /// between two consecutive rows of pixels. - uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } + size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; - void set_transparency(bool transparent) { transparent_ = transparent; } - bool has_transparency() const { return transparent_; } + bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; } #ifdef USE_LVGL lv_img_dsc_t *get_lv_img_dsc(); #endif protected: bool get_binary_pixel_(int x, int y) const; - Color get_rgb24_pixel_(int x, int y) const; - Color get_rgba_pixel_(int x, int y) const; + Color get_rgb_pixel_(int x, int y) const; Color get_rgb565_pixel_(int x, int y) const; Color get_grayscale_pixel_(int x, int y) const; @@ -64,7 +53,9 @@ class Image : public display::BaseImage { int height_; ImageType type_; const uint8_t *data_start_; - bool transparent_; + Transparency transparency_; + size_t bpp_{}; + size_t stride_{}; #ifdef USE_LVGL lv_img_dsc_t dsc_{}; #endif diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 89ec13fe5b..d50b2b483c 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,45 +1,27 @@ #include "json_util.h" #include "esphome/core/log.h" -#ifdef USE_ESP8266 -#include -#endif -#ifdef USE_ESP32 -#include -#endif -#ifdef USE_RP2040 -#include -#endif - namespace esphome { namespace json { static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT +static const auto ALLOCATOR = RAMAllocator(RAMAllocator::ALLOC_INTERNAL); std::string build_json(const json_build_t &f) { // Here we are allocating up to 5kb of memory, // with the heap size minus 2kb to be safe if less than 5kb // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` -#ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) -#elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); -#elif defined(USE_RP2040) - const size_t free_heap = rp2040.getFreeHeap(); -#elif defined(USE_LIBRETINY) - const size_t free_heap = lt_heap_get_free(); -#endif - + auto free_heap = ALLOCATOR.get_max_free_block_size(); size_t request_size = std::min(free_heap, (size_t) 512); while (true) { - ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size); + ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); DynamicJsonDocument json_document(request_size); if (json_document.capacity() == 0) { ESP_LOGE(TAG, - "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + "Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes", request_size, free_heap); return "{}"; } @@ -47,7 +29,7 @@ std::string build_json(const json_build_t &f) { f(root); if (json_document.overflowed()) { if (request_size == free_heap) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes", + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes", free_heap); return "{}"; } @@ -55,7 +37,7 @@ std::string build_json(const json_build_t &f) { continue; } json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); std::string output; serializeJson(json_document, output); return output; @@ -67,20 +49,12 @@ bool parse_json(const std::string &data, const json_parse_t &f) { // with the heap size minus 2kb to be safe if less than that // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` -#ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) -#elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); -#elif defined(USE_RP2040) - const size_t free_heap = rp2040.getFreeHeap(); -#elif defined(USE_LIBRETINY) - const size_t free_heap = lt_heap_get_free(); -#endif + auto free_heap = ALLOCATOR.get_max_free_block_size(); size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); while (true) { DynamicJsonDocument json_document(request_size); if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %zu bytes, free heap: %zu", request_size, free_heap); return false; } diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ceeb30baf5..a090f42aa7 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.reserve(kv.value_len); + stored_data.data.resize(kv.value_len); fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index bad180ce6d..ca32b9c571 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -36,7 +36,7 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast(roun * range as set in the traits, so the output needs to do this. * * For COLD_WARM_WHITE capability: - * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * - cold_white, warm_white: The brightness of the light's cold and warm white channels. * * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 8fdd03f647..c64ffcb5f2 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -23,7 +23,7 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, focused_widgets, update_to_code -from .defines import add_define +from .defines import CONF_DRAW_ROUNDING, add_define from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -197,14 +197,18 @@ def final_validation(configs): for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) - if CONF_LAMBDA in display: + if CONF_LAMBDA in display or CONF_PAGES in display: raise cv.Invalid( - "Using lambda: in display config not compatible with LVGL" + "Using lambda: or pages: in display config is not compatible with LVGL" ) - if display[CONF_AUTO_CLEAR_ENABLED]: + if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( "Using auto_clear_enabled: true in display config not compatible with LVGL" ) + if draw_rounding := display.get(CONF_DRAW_ROUNDING): + config[CONF_DRAW_ROUNDING] = max( + draw_rounding, config[CONF_DRAW_ROUNDING] + ) buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index c26ae54892..168fc03cb7 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -4,24 +4,27 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT -from esphome.cpp_generator import get_variable +from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr from .defines import ( CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE, + CONF_DISP_BG_OPA, CONF_EDITING, CONF_FREEZE, CONF_LVGL_ID, CONF_SHOW_SNOW, + PARTS, literal, ) -from .lv_validation import lv_bool, lv_color, lv_image +from .lv_validation import lv_bool, lv_color, lv_image, opacity from .lvcode import ( LVGL_COMP_ARG, UPDATE_EVENT, LambdaContext, LocalVariable, + LvglComponent, ReturnStatement, add_line_marks, lv, @@ -31,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, @@ -39,6 +42,7 @@ from .types import ( ObjUpdateAction, lv_disp_t, lv_group_t, + lv_obj_base_t, lv_obj_t, lv_pseudo_button_t, ) @@ -92,7 +96,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: lv_add(ReturnStatement(lvgl_comp.is_paused())) - var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(LvglComponent, *template_arg), + await context.get_lambda(), + ) await cg.register_parented(var, lvgl) return var @@ -113,19 +121,32 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) - var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(LvglComponent, *template_arg), + await context.get_lambda(), + ) await cg.register_parented(var, lvgl) return var async def disp_update(disp, config: dict): - if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: + if ( + CONF_DISP_BG_COLOR not in config + and CONF_DISP_BG_IMAGE not in config + and CONF_DISP_BG_OPA not in config + ): return with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) if bg_image := config.get(CONF_DISP_BG_IMAGE): - lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + if bg_image == "none": + lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr")) + else: + lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None: + lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa)) @automation.register_action( @@ -317,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/defines.py b/esphome/components/lvgl/defines.py index 5371f110a6..733a6bc180 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -168,6 +168,7 @@ LV_EVENT_MAP = { "READY": "READY", "CANCEL": "CANCEL", "ALL_EVENTS": "ALL", + "CHANGE": "VALUE_CHANGED", } LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) @@ -214,7 +215,7 @@ LV_LONG_MODES = LvConstant( ) STATES = ( - "default", + # default state not included here "checked", "focused", "focus_key", @@ -402,6 +403,7 @@ CONF_COLUMN = "column" CONF_DIGITS = "digits" CONF_DISP_BG_COLOR = "disp_bg_color" CONF_DISP_BG_IMAGE = "disp_bg_image" +CONF_DISP_BG_OPA = "disp_bg_opa" CONF_BODY = "body" CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 61bdfe9755..a9fe56fb32 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev } void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); + page->set_parent(this); page->setup(this->pages_.size() - 1); } void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { @@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } +size_t LvglComponent::get_current_page() const { return this->current_page_; } +bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); @@ -498,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); } void *lv_custom_mem_alloc(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); } return ptr; } @@ -517,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) { ptr = heap_caps_malloc(size, cap_bits); } if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); return nullptr; } -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); return ptr; } void lv_custom_mem_free(void *ptr) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); if (ptr == nullptr) return; heap_caps_free(ptr); } void *lv_custom_mem_realloc(void *ptr, size_t size) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); -#endif + ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); return heap_caps_realloc(ptr, size, cap_bits); } #endif diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 56413ad77e..69fa808d53 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) { inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) { lv_disp_set_bg_image(disp, image->get_lv_img_dsc()); } + +inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector); +} +#ifdef USE_LVGL_METER +inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src, + lv_coord_t pivot_x, lv_coord_t pivot_y) { + return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y); +} +#endif // USE_LVGL_METER #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector images) { @@ -84,7 +94,9 @@ class LvCompound { lv_obj_t *obj{}; }; -class LvPageType { +class LvglComponent; + +class LvPageType : public Parented { public: LvPageType(bool skip) : skip(skip) {} @@ -92,6 +104,9 @@ class LvPageType { this->index = index; this->obj = lv_obj_create(nullptr); } + + bool is_showing() const; + lv_obj_t *obj{}; size_t index{}; bool skip; @@ -178,6 +193,7 @@ class LvglComponent : public PollingComponent { void show_next_page(lv_scr_load_anim_t anim, uint32_t time); void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + size_t get_current_page() const; void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } void restore_focus_mark(lv_group_t *group) { auto *mark = this->focus_marks_[group]; @@ -241,14 +257,13 @@ template class LvglAction : public Action, public Parente std::function action_{}; }; -template class LvglCondition : public Condition, public Parented { +template class LvglCondition : public Condition, public Parented { public: - LvglCondition(std::function &&condition_lambda) - : condition_lambda_(std::move(condition_lambda)) {} + LvglCondition(std::function &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {} bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } protected: - std::function condition_lambda_{}; + std::function condition_lambda_{}; }; #ifdef USE_LVGL_TOUCHSCREEN diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 3f56b3345f..f0318dd17a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_color, lv_font, lv_gradient, lv_image +from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( LVEncoderListener, @@ -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,11 +351,13 @@ FLEX_OBJ_SCHEMA = { cv.Optional(df.CONF_FLEX_GROW): cv.int_, } - DISP_BG_SCHEMA = cv.Schema( { - cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, + cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( + cv.one_of("none", lower=True), lv_image + ), cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, + cv.Optional(df.CONF_DISP_BG_OPA): opacity, } ) 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/img.py b/esphome/components/lvgl/widgets/img.py index 59b2c97c63..46077190d0 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -79,7 +79,7 @@ class ImgType(WidgetType): if CONF_ANTIALIAS in config: lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) if mode := config.get(CONF_MODE): - lv.img_set_mode(w.obj, mode) + await w.set_property("size_mode", mode) img_spec = ImgType() 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/meter.py b/esphome/components/lvgl/widgets/meter.py index cd61d1c775..29a382f7cf 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -27,7 +27,7 @@ from ..defines import ( CONF_START_VALUE, CONF_TICKS, ) -from ..helpers import add_lv_use +from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( angle, get_end_value, @@ -182,6 +182,7 @@ class MeterType(WidgetType): async def to_code(self, w: Widget, config): """For a meter object, create and set parameters""" + lvgl_components_required.add(CONF_METER) var = w.obj for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 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/page.py b/esphome/components/lvgl/widgets/page.py index a754a9cb9a..23c162e010 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -2,6 +2,7 @@ from esphome import automation, codegen as cg from esphome.automation import Trigger import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID +from esphome.cpp_generator import MockObj, TemplateArguments from ..defines import ( CONF_ANIMATION, @@ -17,18 +18,28 @@ from ..lvcode import ( EVENT_ARG, LVGL_COMP_ARG, LambdaContext, + ReturnStatement, add_line_marks, lv_add, lvgl_comp, lvgl_static, ) from ..schemas import LVGL_SCHEMA -from ..types import LvglAction, lv_page_t -from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from ..types import LvglAction, LvglCondition, lv_page_t +from . import ( + Widget, + WidgetType, + add_widgets, + get_widgets, + set_obj_properties, + wait_for_widgets, +) CONF_ON_LOAD = "on_load" CONF_ON_UNLOAD = "on_unload" +PAGE_ARG = "_page" + PAGE_SCHEMA = cv.Schema( { cv.Optional(CONF_SKIP, default=False): lv_bool, @@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args): return var +@automation.register_condition( + "lvgl.page.is_showing", + LvglCondition, + cv.maybe_simple_value( + cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}), + key=CONF_ID, + ), +) +async def page_is_showing_to_code(config, condition_id, template_arg, args): + await wait_for_widgets() + page = await cg.get_variable(config[CONF_ID]) + async with LambdaContext( + [(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing())) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(lv_page_t, *template_arg), + await context.get_lambda(), + ) + await cg.register_parented(var, page) + return var + + @automation.register_action( "lvgl.page.previous", LvglAction, 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/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index dcc96102db..28674acd96 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -11,15 +11,17 @@ void MicroNovaSwitch::write_state(bool state) { if (this->micronova_->get_current_stove_state() == 0) { this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); this->publish_state(true); - } else + } else { ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state()); + } } else { // don't send power-off when status is Off or Final cleaning if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) { this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); this->publish_state(false); - } else + } else { ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state()); + } } this->micronova_->update(); break; diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index 5084fd1eec..e6fffa2511 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -19,10 +19,12 @@ template class MideaActionBase : public Action { template class FollowMeAction : public MideaActionBase { TEMPLATABLE_VALUE(float, temperature) + TEMPLATABLE_VALUE(bool, use_fahrenheit) TEMPLATABLE_VALUE(bool, beeper) void play(Ts... x) override { - this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); + this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...), + this->beeper_.value(x...)); } }; diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index a823680d03..247aea0488 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -1,6 +1,7 @@ #ifdef USE_ARDUINO #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include "air_conditioner.h" #include "ac_adapter.h" #include @@ -121,7 +122,7 @@ void AirConditioner::dump_config() { /* ACTIONS */ -void AirConditioner::do_follow_me(float temperature, bool beeper) { +void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER // Check if temperature is finite (not NaN or infinite) if (!std::isfinite(temperature)) { @@ -131,13 +132,14 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) { // Round and convert temperature to long, then clamp and convert it to uint8_t uint8_t temp_uint8 = - static_cast(std::max(0L, std::min(static_cast(UINT8_MAX), std::lroundf(temperature)))); + static_cast(esphome::clamp(std::lroundf(temperature), 0L, static_cast(UINT8_MAX))); - ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature, - temp_uint8); + char temp_symbol = use_fahrenheit ? 'F' : 'C'; + ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature, + temp_symbol, temp_uint8, temp_symbol); // Create and transmit the data - IrFollowMeData data(temp_uint8, beeper); + IrFollowMeData data(temp_uint8, use_fahrenheit, beeper); this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index d809aa78f6..e70bd34e71 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -32,7 +32,7 @@ class AirConditioner : public ApplianceBase, /* ### ACTIONS ### */ /* ############### */ - void do_follow_me(float temperature, bool beeper = false); + void do_follow_me(float temperature, bool use_fahrenheit, bool beeper = false); void do_display_toggle(); void do_swing_step(); void do_beeper_on() { this->set_beeper_feedback(true); } diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index e5612796a3..b7fef5e1ab 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TIMEOUT, CONF_TEMPERATURE, + CONF_USE_FAHRENHEIT, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -172,11 +173,10 @@ MIDEA_ACTION_BASE_SCHEMA = cv.Schema( ) # FollowMe action -MIDEA_FOLLOW_ME_MIN = 0 -MIDEA_FOLLOW_ME_MAX = 37 MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( { cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.templatable(cv.boolean), cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), } ) @@ -186,6 +186,8 @@ MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( async def follow_me_to_code(var, config, args): template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_USE_FAHRENHEIT], args, cg.bool_) + cg.add(var.set_use_fahrenheit(template_)) template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) cg.add(var.set_temperature(template_)) diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index a8b89f9b7b..eba8fc87f7 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -16,22 +16,53 @@ class IrFollowMeData : public IrData { IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} // Copy from Base IrFollowMeData(const IrData &data) : IrData(data) {} - // Direct from temperature and beeper values + // Direct from temperature in celsius and beeper values IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { - this->set_temp(temp); + this->set_temp(temp, false); + this->set_beeper(beeper); + } + // Direct from temperature, fahrenheit and beeper values + IrFollowMeData(uint8_t temp, bool fahrenheit, bool beeper) : IrFollowMeData() { + this->set_temp(temp, fahrenheit); this->set_beeper(beeper); } /* TEMPERATURE */ - uint8_t temp() const { return this->get_value_(4) - 1; } - void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } + uint8_t temp() const { + if (this->fahrenheit()) { + return this->get_value_(4) + 31; + } + return this->get_value_(4) - 1; + } + void set_temp(uint8_t val, bool fahrenheit = false) { + this->set_fahrenheit(fahrenheit); + if (this->fahrenheit()) { + // see https://github.com/esphome/feature-requests/issues/1627#issuecomment-1365639966 + val = esphome::clamp(val, MIN_TEMP_F, MAX_TEMP_F) - 31; + } else { + val = esphome::clamp(val, MIN_TEMP_C, MAX_TEMP_C) + 1; + } + this->set_value_(4, val); + } /* BEEPER */ bool beeper() const { return this->get_value_(3, 128); } void set_beeper(bool val) { this->set_mask_(3, val, 128); } + /* FAHRENHEIT */ + bool fahrenheit() const { return this->get_value_(2, 32); } + void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); } + protected: - static const uint8_t MAX_TEMP = 37; + static const uint8_t MIN_TEMP_C = 0; + static const uint8_t MAX_TEMP_C = 37; + + // see + // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116 + static const uint8_t MIN_TEMP_F = 32; + // see + // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117 + static const uint8_t MAX_TEMP_F = 99; }; class IrSpecialData : public IrData { diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2b0d941220..e1002478a1 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -373,7 +373,7 @@ async def to_code(config): ) ) - cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name)) if config[CONF_USE_ABBREVIATIONS]: cg.add_define("USE_MQTT_ABBREVIATIONS") diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c7ace505a8..9afa3a588d 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -606,7 +606,13 @@ void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); } void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); } const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } -void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; } +void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) { + if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) { + this->topic_prefix_ = str_sanitize(App.get_name()); + } else { + this->topic_prefix_ = topic_prefix; + } +} const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 34eac29464..c68b3c62eb 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -165,7 +165,7 @@ class MQTTClientComponent : public Component { * * @param topic_prefix The topic prefix. The last "/" is appended automatically. */ - void set_topic_prefix(const std::string &topic_prefix); + void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix); /// Get the topic prefix of this device, using default if necessary const std::string &get_topic_prefix() const; diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 65f1fd0058..c718355af8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -49,6 +49,23 @@ class TouchTrigger : public Trigger { } }; +template class NextionSetBrightnessAction : public Action { + public: + explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {} + + TEMPLATABLE_VALUE(float, brightness) + + void play(Ts... x) override { + this->component_->set_brightness(this->brightness_.value(x...)); + this->component_->set_backlight_brightness(this->brightness_.value(x...)); + } + + void set_brightness(std::function brightness) { this->brightness_ = brightness; } + + protected: + Nextion *component_; +}; + template class NextionPublishFloatAction : public Action { public: explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {} diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 6f284376af..60f26e5234 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,30 +1,30 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import display, uart -from esphome.components import esp32 +import esphome.codegen as cg +from esphome.components import display, esp32, uart +import esphome.config_validation as cv from esphome.const import ( + CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, - CONF_BRIGHTNESS, - CONF_TRIGGER_ID, CONF_ON_TOUCH, + CONF_TRIGGER_ID, ) from esphome.core import CORE + from . import Nextion, nextion_ns, nextion_ref from .base_component import ( + CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, CONF_ON_BUFFER_OVERFLOW, + CONF_ON_PAGE, + CONF_ON_SETUP, CONF_ON_SLEEP, CONF_ON_WAKE, - CONF_ON_SETUP, - CONF_ON_PAGE, + CONF_SKIP_CONNECTION_HANDSHAKE, + CONF_START_UP_PAGE, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, - CONF_START_UP_PAGE, - CONF_AUTO_WAKE_ON_TOUCH, - CONF_EXIT_REPARSE_ON_START, - CONF_SKIP_CONNECTION_HANDSHAKE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] @@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] +NextionSetBrightnessAction = nextion_ns.class_( + "NextionSetBrightnessAction", automation.Action +) SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) @@ -46,7 +49,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Nextion), cv.Optional(CONF_TFT_URL): cv.url, - cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_BRIGHTNESS): cv.percentage, cv.Optional(CONF_ON_SETUP): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), @@ -92,12 +95,34 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "display.nextion.set_brightness", + NextionSetBrightnessAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Nextion), + cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + }, + key=CONF_BRIGHTNESS, + ), +) +async def nextion_set_brightness_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) + cg.add(var.set_brightness(template_)) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await uart.register_uart_device(var, config) if CONF_BRIGHTNESS in config: cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void @@ -119,17 +144,17 @@ async def to_code(config): cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: - cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) + cg.add(var.set_touch_sleep_timeout(config[CONF_TOUCH_SLEEP_TIMEOUT])) if CONF_WAKE_UP_PAGE in config: - cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE])) + cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE])) if CONF_START_UP_PAGE in config: - cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE])) + cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE])) - cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) - cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) + cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START])) cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 50a5834347..67f08f68f8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -40,7 +40,7 @@ bool Nextion::send_command_(const std::string &command) { } bool Nextion::check_connect_() { - if (this->get_is_connected_()) + if (this->is_connected_) return true; // Check if the handshake should be skipped for the Nextion connection @@ -273,21 +273,15 @@ void Nextion::loop() { this->sent_setup_commands_ = true; this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. - this->set_backlight_brightness(this->brightness_); + if (this->brightness_.has_value()) { + this->set_backlight_brightness(this->brightness_.value()); + } // Check if a startup page has been set and send the command if (this->start_up_page_ != -1) { this->goto_page(this->start_up_page_); } - // This could probably be removed from the loop area, as those are redundant. - this->set_auto_wake_on_touch(this->auto_wake_on_touch_); - this->set_exit_reparse_on_start(this->exit_reparse_on_start_); - - if (this->touch_sleep_timeout_ != 0) { - this->set_touch_sleep_timeout(this->touch_sleep_timeout_); - } - if (this->wake_up_page_ != -1) { this->set_wake_up_page(this->wake_up_page_); } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f539c79718..b2404e1f0d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -856,76 +856,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_backlight_brightness(float brightness); - /** - * Set the touch sleep timeout of the display. - * @param timeout Timeout in seconds. - * - * Example: - * ```cpp - * it.set_touch_sleep_timeout(30); - * ``` - * - * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up - * `thup`. - */ - void set_touch_sleep_timeout(uint16_t timeout); - - /** - * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. - * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to - * wakes up to current page. - * - * Example: - * ```cpp - * it.set_wake_up_page(2); - * ``` - * - * The display will wake up to page 2. - */ - void set_wake_up_page(uint8_t page_id = 255); - - /** - * Sets which page Nextion loads when connecting to ESPHome. - * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to - * wakes up to current page. - * - * Example: - * ```cpp - * it.set_start_up_page(2); - * ``` - * - * The display will go to page 2 when it establishes a connection to ESPHome. - */ - void set_start_up_page(uint8_t page_id = 255); - - /** - * Sets if Nextion should auto-wake from sleep when touch press occurs. - * @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode, - * the first touch will only trigger the auto wake mode and not trigger a Touch Event. - * - * Example: - * ```cpp - * it.set_auto_wake_on_touch(true); - * ``` - * - * The display will wake up by touch. - */ - void set_auto_wake_on_touch(bool auto_wake); - - /** - * Sets if Nextion should exit the active reparse mode before the "connect" command is sent - * @param exit_reparse True or false. When exit_reparse is true, the exit reparse command - * will be sent before requesting the connection from Nextion. - * - * Example: - * ```cpp - * it.set_exit_reparse_on_start(true); - * ``` - * - * The display will be requested to leave active reparse mode before setup. - */ - void set_exit_reparse_on_start(bool exit_reparse); - /** * Sets whether the Nextion display should skip the connection handshake process. * @param skip_handshake True or false. When skip_connection_handshake is true, @@ -1172,15 +1102,75 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void update_components_by_prefix(const std::string &prefix); - void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) { - this->touch_sleep_timeout_ = touch_sleep_timeout; - } - void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } - void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; } - void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } - void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { - this->exit_reparse_on_start_ = exit_reparse_on_start; - } + /** + * Set the touch sleep timeout of the display. + * @param timeout Timeout in seconds. + * + * Example: + * ```cpp + * it.set_touch_sleep_timeout(30); + * ``` + * + * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up + * `thup`. + */ + void set_touch_sleep_timeout(uint32_t touch_sleep_timeout); + + /** + * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. + * @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_wake_up_page(2); + * ``` + * + * The display will wake up to page 2. + */ + void set_wake_up_page(uint8_t wake_up_page = 255); + + /** + * Sets which page Nextion loads when connecting to ESPHome. + * @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_start_up_page(2); + * ``` + * + * The display will go to page 2 when it establishes a connection to ESPHome. + */ + void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; } + + /** + * Sets if Nextion should auto-wake from sleep when touch press occurs. + * @param auto_wake_on_touch True or false. When auto_wake is true and Nextion is in sleep mode, + * the first touch will only trigger the auto wake mode and not trigger a Touch Event. + * + * Example: + * ```cpp + * it.set_auto_wake_on_touch(true); + * ``` + * + * The display will wake up by touch. + */ + void set_auto_wake_on_touch(bool auto_wake_on_touch); + + /** + * Sets if Nextion should exit the active reparse mode before the "connect" command is sent + * @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command + * will be sent before requesting the connection from Nextion. + * + * Example: + * ```cpp + * it.set_exit_reparse_on_start(true); + * ``` + * + * The display will be requested to leave active reparse mode before setup. + */ + void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; } /** * @brief Retrieves the number of commands pending in the Nextion command queue. @@ -1217,6 +1207,25 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ bool is_updating() override; + /** + * @brief Check if the Nextion display is successfully connected. + * + * This method returns whether a successful connection has been established with + * the Nextion display. A connection is considered established when: + * + * - The initial handshake with the display is completed successfully, or + * - The handshake is skipped via skip_connection_handshake_ flag + * + * The connection status is particularly useful when: + * - Troubleshooting communication issues + * - Ensuring the display is ready before sending commands + * - Implementing connection-dependent behaviors + * + * @return true if the Nextion display is connected and ready to receive commands + * @return false if the display is not yet connected or connection was lost + */ + bool is_connected() { return this->is_connected_; } + protected: std::deque nextion_queue_; std::deque waveform_queue_; @@ -1315,8 +1324,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #endif // USE_NEXTION_TFT_UPLOAD - bool get_is_connected_() { return this->is_connected_; } - bool check_connect_(); std::vector touch_; @@ -1332,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager buffer_overflow_callback_{}; optional writer_; - float brightness_{1.0}; + optional brightness_; std::string device_model_; std::string firmware_version_; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 398e9dd502..e3172c8c1b 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -10,19 +10,19 @@ static const char *const TAG = "nextion"; // Sleep safe commands void Nextion::soft_reset() { this->send_command_("rest"); } -void Nextion::set_wake_up_page(uint8_t page_id) { - this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); +void Nextion::set_wake_up_page(uint8_t wake_up_page) { + this->wake_up_page_ = wake_up_page; + this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); } -void Nextion::set_start_up_page(uint8_t page_id) { this->start_up_page_ = page_id; } - -void Nextion::set_touch_sleep_timeout(uint16_t timeout) { - if (timeout < 3) { +void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) { + if (touch_sleep_timeout < 3) { ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); return; } - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true); + this->touch_sleep_timeout_ = touch_sleep_timeout; + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true); } void Nextion::sleep(bool sleep) { @@ -54,7 +54,6 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { this->ignore_is_setup_ = false; return all_commands_sent; } -void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; } // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { @@ -191,8 +190,9 @@ void Nextion::set_backlight_brightness(float brightness) { this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast(brightness * 100)); } -void Nextion::set_auto_wake_on_touch(bool auto_wake) { - this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); +void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { + this->auto_wake_on_touch_ = auto_wake_on_touch; + this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); } // General Component 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/online_image/__init__.py b/esphome/components/online_image/__init__.py index be1bfb4a00..d1915c7364 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -4,14 +4,18 @@ from esphome import automation import esphome.codegen as cg from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( + CONF_INVERT_ALPHA, CONF_USE_TRANSPARENCY, - IMAGE_TYPE, + IMAGE_SCHEMA, Image_, - validate_cross_dependencies, + get_image_type_enum, + get_transparency_enum, ) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_DITHER, + CONF_FILE, CONF_FORMAT, CONF_ID, CONF_ON_ERROR, @@ -23,7 +27,7 @@ from esphome.const import ( AUTO_LOAD = ["image"] DEPENDENCIES = ["display", "http_request"] -CODEOWNERS = ["@guillempages"] +CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" @@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image") ImageFormat = online_image_ns.enum("ImageFormat") -FORMAT_PNG = "PNG" -IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here +class Format: + def __init__(self, image_type): + self.image_type = image_type + + @property + def enum(self): + return getattr(ImageFormat, self.image_type) + + def actions(self): + pass + + +class PNGFormat(Format): + def __init__(self): + super().__init__("PNG") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + +# New formats can be added here. +IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) @@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_( "DownloadErrorTrigger", automation.Trigger.template() ) -ONLINE_IMAGE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), - # - # Common image options - # - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), - # Not setting default here on purpose; the default depends on the image type, - # and thus will be set in the "validate_cross_dependencies" validator. - cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, - # - # Online Image specific options - # - cv.Required(CONF_URL): cv.url, - cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), - } - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), - } - ), + +def remove_options(*options): + return { + cv.Optional(option): cv.invalid( + f"{option} is an invalid option for online_image" + ) + for option in options } -).extend(cv.polling_component_schema("never")) + + +ONLINE_IMAGE_SCHEMA = ( + IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + .extend( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # Online Image specific options + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DownloadFinishedTrigger + ), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), + } + ), + } + ) + .extend(cv.polling_component_schema("never")) +) CONFIG_SCHEMA = cv.Schema( cv.All( ONLINE_IMAGE_SCHEMA, - validate_cross_dependencies, cv.require_framework_version( # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed # esp8266_arduino=cv.Version(2, 7, 0), esp32_arduino=cv.Version(0, 0, 0), esp_idf=cv.Version(4, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), + host=cv.Version(0, 0, 0), ), ) ) @@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - format = config[CONF_FORMAT] - if format in [FORMAT_PNG]: - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.0.2") + image_format = IMAGE_FORMATS[config[CONF_FORMAT]] + image_format.actions() url = config[CONF_URL] width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = config[CONF_USE_TRANSPARENCY] + transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY]) var = cg.new_Pvariable( config[CONF_ID], url, width, height, - format, - config[CONF_TYPE], + image_format.enum, + get_image_type_enum(config[CONF_TYPE]), + transparent, config[CONF_BUFFER_SIZE], ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) - cg.add(var.set_transparency(transparent)) - if placeholder_id := config.get(CONF_PLACEHOLDER): placeholder = await cg.get_variable(placeholder_id) cg.add(var.set_placeholder(placeholder)) diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 908efab987..cde7f572e3 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -1,5 +1,4 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/color.h" namespace esphome { @@ -23,7 +22,7 @@ class ImageDecoder { /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be download for the image. + * @param download_size The total number of bytes that need to be downloaded for the image. */ virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } @@ -38,7 +37,7 @@ class ImageDecoder { * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully * decode anything, or negative in case of a decoding error. */ - virtual int decode(uint8_t *buffer, size_t size); + virtual int decode(uint8_t *buffer, size_t size) = 0; /** * @brief Request the image to be resized once the actual dimensions are known. @@ -50,7 +49,7 @@ class ImageDecoder { void set_size(int width, int height); /** - * @brief Draw a rectangle on the display_buffer using the defined color. + * @brief Fill a rectangle on the display_buffer using the defined color. * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. * In case of binary displays, the color will be converted to binary as well. * Called by the callback functions, to be able to access the parent Image class. @@ -59,7 +58,7 @@ class ImageDecoder { * @param y The top-most coordinate of the rectangle. * @param w The width of the rectangle. * @param h The height of the rectangle. - * @param color The color to draw the rectangle with. + * @param color The fill color */ void draw(int x, int y, int w, int h, const Color &color); @@ -67,7 +66,7 @@ class ImageDecoder { protected: OnlineImage *image_; - // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Will be overwritten anyway once the download size is known. uint32_t download_size_ = 1; uint32_t decoded_bytes_ = 0; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 1786809dfa..93d070c6a9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) { } OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - uint32_t download_buffer_size) - : Image(nullptr, 0, 0, type), + image::Transparency transparency, uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), format_(format), @@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, void OnlineImage::release() { if (this->buffer_) { - ESP_LOGD(TAG, "Deallocating old buffer..."); + ESP_LOGV(TAG, "Deallocating old buffer..."); this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); this->data_start_ = nullptr; this->buffer_ = nullptr; @@ -70,28 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) { if (this->buffer_) { return false; } - auto new_size = this->get_buffer_size_(width, height); - ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); - delay_microseconds_safe(2000); + size_t new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_) { - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGD(TAG, "New size: (%d, %d)", width, height); - } else { -#if defined(USE_ESP8266) - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - int max_block = ESP.getMaxFreeBlockSize(); -#elif defined(USE_ESP32) - int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); -#else - int max_block = -1; -#endif - ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, + this->allocator_.get_max_free_block_size()); this->end_connection_(); return false; } + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGV(TAG, "New size: (%d, %d)", width, height); return true; } @@ -99,9 +90,8 @@ void OnlineImage::update() { if (this->decoder_) { ESP_LOGW(TAG, "Image already being updated."); return; - } else { - ESP_LOGI(TAG, "Updating image"); } + ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); this->downloader_ = this->parent_->get(this->url_); @@ -150,10 +140,11 @@ void OnlineImage::loop() { return; } if (!this->downloader_ || this->decoder_->is_finished()) { - ESP_LOGD(TAG, "Image fully downloaded"); this->data_start_ = buffer_; this->width_ = buffer_width_; this->height_ = buffer_height_; + ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), + this->width_, this->height_); this->end_connection_(); this->download_finished_callback_.call(); return; @@ -179,6 +170,19 @@ void OnlineImage::loop() { } } +void OnlineImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + void OnlineImage::draw_pixel_(int x, int y, Color color) { if (!this->buffer_) { ESP_LOGE(TAG, "Buffer not allocated!"); @@ -192,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { switch (this->type_) { case ImageType::IMAGE_TYPE_BINARY: { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t pos = x + y * width_8; - if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { - this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; } else { - this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + this->buffer_[pos] &= ~bitno; } break; } case ImageType::IMAGE_TYPE_GRAYSCALE: { uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->has_transparency()) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (gray == 1) { gray = 0; } if (color.w < 0x80) { gray = 1; } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; } this->buffer_[pos] = gray; break; } case ImageType::IMAGE_TYPE_RGB565: { + this->map_chroma_key(color); uint16_t col565 = display::ColorUtil::color_to_565(color); this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - if (this->has_transparency()) + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 2] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGBA: { - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - this->buffer_[pos + 3] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGB24: - default: { - if (this->has_transparency()) { - if (color.b == 1 && color.r == 0 && color.g == 0) { - color.b = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = 0; - color.b = 1; - } } + break; + } + case ImageType::IMAGE_TYPE_RGB: { + this->map_chroma_key(color); this->buffer_[pos + 0] = color.r; this->buffer_[pos + 1] = color.g; this->buffer_[pos + 2] = color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } break; } } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 017402a088..e044b4f390 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent, * @param buffer_size Size of the buffer used to download the image. */ OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - uint32_t buffer_size); + image::Transparency transparency, uint32_t buffer_size); void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void update() override; void loop() override; + void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index a928276dcc..d82ff93149 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -1,6 +1,7 @@ #pragma once #include "image_decoder.h" +#include "esphome/core/defines.h" #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 81cd78af08..42b476eb87 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -1,10 +1,12 @@ from typing import Any +import logging +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266, CONF_TRIGGER_ID from . import const, schema, validate, generate CODEOWNERS = ["@olegtarasov"] @@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" -CONF_OPENTHERM_VERSION = "opentherm_version" +CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed +CONF_BEFORE_SEND = "before_send" +CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" + +# Triggers +BeforeSendTrigger = generate.opentherm_ns.class_( + "BeforeSendTrigger", + automation.Trigger.template(generate.OpenthermData.operator("ref")), +) +BeforeProcessResponseTrigger = generate.opentherm_ns.class_( + "BeforeProcessResponseTrigger", + automation.Trigger.template(generate.OpenthermData.operator("ref")), +) + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, + cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated + cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), + } + ), + cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BeforeProcessResponseTrigger + ), + } + ), } ) .extend( @@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All( schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) ) ) + .extend( + validate.create_entities_schema( + schema.SETTINGS, (lambda s: s.validation_schema) + ) + ) .extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), ) @@ -60,18 +93,33 @@ async def to_code(config: dict[str, Any]) -> None: out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) cg.add(var.set_out_pin(out_pin)) - non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} + non_sensors = { + CONF_ID, + CONF_IN_PIN, + CONF_OUT_PIN, + CONF_BEFORE_SEND, + CONF_BEFORE_PROCESS_RESPONSE, + } input_sensors = [] + settings = [] for key, value in config.items(): if key in non_sensors: continue if key in schema.INPUTS: input_sensor = await cg.get_variable(value) - cg.add( - getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) - ) + cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor)) input_sensors.append(key) + elif key in schema.SETTINGS: + if value == schema.SETTINGS[key].default_value: + continue + cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) + settings.append(key) else: + if key == CONF_OPENTHERM_VERSION: + _LOGGER.warning( + "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" + "Please change to 'opentherm_version_controller'." + ) cg.add(getattr(var, f"set_{key}")(value)) if len(input_sensors) > 0: @@ -81,3 +129,21 @@ async def to_code(config: dict[str, Any]) -> None: ) generate.define_readers(const.INPUT_SENSOR, input_sensors) generate.add_messages(var, input_sensors, schema.INPUTS) + + if len(settings) > 0: + generate.define_has_settings(settings, schema.SETTINGS) + generate.define_message_handler(const.SETTING, settings, schema.SETTINGS) + generate.define_setting_readers(const.SETTING, settings) + generate.add_messages(var, settings, schema.SETTINGS) + + for conf in config.get(CONF_BEFORE_SEND, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(generate.OpenthermData.operator("ref"), "x")], conf + ) + + for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(generate.OpenthermData.operator("ref"), "x")], conf + ) diff --git a/esphome/components/opentherm/automation.h b/esphome/components/opentherm/automation.h new file mode 100644 index 0000000000..acbe33ac8f --- /dev/null +++ b/esphome/components/opentherm/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "hub.h" +#include "opentherm.h" + +namespace esphome { +namespace opentherm { + +class BeforeSendTrigger : public Trigger { + public: + BeforeSendTrigger(OpenthermHub *hub) { + hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); }); + } +}; + +class BeforeProcessResponseTrigger : public Trigger { + public: + BeforeProcessResponseTrigger(OpenthermHub *hub) { + hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); }); + } +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py index a113331585..51ad84ce46 100644 --- a/esphome/components/opentherm/const.py +++ b/esphome/components/opentherm/const.py @@ -9,3 +9,4 @@ SWITCH = "switch" NUMBER = "number" OUTPUT = "output" INPUT_SENSOR = "input_sensor" +SETTING = "setting" diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 9716cab093..6b6a0255a8 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -1,13 +1,14 @@ from collections.abc import Awaitable -from typing import Any, Callable +from typing import Any, Callable, Optional import esphome.codegen as cg from esphome.const import CONF_ID from . import const -from .schema import TSchema +from .schema import TSchema, SettingSchema opentherm_ns = cg.esphome_ns.namespace("opentherm") OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) +OpenthermData = opentherm_ns.class_("OpenthermData") def define_has_component(component_type: str, keys: list[str]) -> None: @@ -21,6 +22,24 @@ def define_has_component(component_type: str, keys: list[str]) -> None: cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") +# We need a separate set of macros for settings because there are different backing field types we need to take +# into account +def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> None: + cg.add_define( + "OPENTHERM_SETTING_LIST(F, sep)", + cg.RawExpression( + " sep ".join( + map( + lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", + keys, + ) + ) + ), + ) + for key in keys: + cg.add_define(f"OPENTHERM_HAS_SETTING_{key}") + + def define_message_handler( component_type: str, keys: list[str], schemas: dict[str, TSchema] ) -> None: @@ -74,16 +93,30 @@ def define_readers(component_type: str, keys: list[str]) -> None: ) -def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): - messages: set[tuple[str, bool]] = set() +def define_setting_readers(component_type: str, keys: list[str]) -> None: for key in keys: - messages.add((schemas[key].message, schemas[key].keep_updated)) - for msg, keep_updated in messages: + cg.add_define( + f"OPENTHERM_READ_{key}", + cg.RawExpression(f"this->{key}_{component_type.lower()}"), + ) + + +def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): + messages: dict[str, tuple[bool, Optional[int]]] = {} + for key in keys: + messages[schemas[key].message] = ( + schemas[key].keep_updated, + schemas[key].order if hasattr(schemas[key], "order") else None, + ) + for msg, (keep_updated, order) in messages.items(): msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") if keep_updated: cg.add(hub.add_repeating_message(msg_expr)) else: - cg.add(hub.add_initial_message(msg_expr)) + if order is not None: + cg.add(hub.add_initial_message(msg_expr, order)) + else: + cg.add(hub.add_initial_message(msg_expr)) def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index aac2966ed1..97adf71752 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); } OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OpenthermData data; data.type = 0; - data.id = 0; + data.id = request_id; data.valueHB = 0; data.valueLB = 0; @@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { // NOLINTEND data.type = MessageType::READ_DATA; - data.id = MessageId::STATUS; data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | (summer_mode_is_active << 5) | (dhw_blocked << 6); return data; } - // Another special case is OpenTherm version number which is configured at hub level as a constant - if (request_id == MessageId::OT_VERSION_CONTROLLER) { - data.type = MessageType::WRITE_DATA; - data.id = MessageId::OT_VERSION_CONTROLLER; - data.f88(this->opentherm_version_); - - return data; - } - -// Disable incomplete switch statement warnings, because the cases in each -// switch are generated based on the configured sensors and inputs. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wswitch" - - // Next, we start with the write requests from switches and other inputs, + // Next, we start with write requests from switches and other inputs, // because we would want to write that data if it is available, rather than // request a read for that type (in the case that both read and write are // supported). @@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + OPENTHERM_SETTING_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_SETTING, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + default: + break; } // Finally, handle the simple read requests, which only change with the message id. - switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } + switch (request_id) { + OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) + default: + break; + } switch (request_id) { OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) + default: + break; } -#pragma GCC diagnostic pop // And if we get here, a message was requested which somehow wasn't handled. // This shouldn't happen due to the way the defines are configured, so we @@ -163,19 +157,37 @@ void OpenthermHub::setup() { // communicate at least once every second. Sending the status request is // good practice anyway. this->add_repeating_message(MessageId::STATUS); - - // Also ensure that we start communication with the STATUS message - this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS); - - if (this->opentherm_version_ > 0.0f) { - this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER); - } - - this->current_message_iterator_ = this->initial_messages_.begin(); + this->write_initial_messages_(this->messages_); + this->message_iterator_ = this->messages_.begin(); } void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } +// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) +void OpenthermHub::write_initial_messages_(std::vector &target) { // NOLINT + std::vector> sorted; + std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted), + [](const std::pair &pair) { return pair.second < REPEATING_MESSAGE_ORDER; }); + std::sort(sorted.begin(), sorted.end(), + [](const std::pair &a, const std::pair &b) { + return a.second < b.second; + }); + + target.clear(); + std::transform(sorted.begin(), sorted.end(), std::back_inserter(target), + [](const std::pair &pair) { return pair.first; }); +} + +// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) +void OpenthermHub::write_repeating_messages_(std::vector &target) { // NOLINT + target.clear(); + for (auto const &pair : this->configured_messages_) { + if (pair.second == REPEATING_MESSAGE_ORDER) { + target.push_back(pair.first); + } + } +} + void OpenthermHub::loop() { if (this->sync_mode_) { this->sync_loop_(); @@ -184,29 +196,18 @@ void OpenthermHub::loop() { auto cur_time = millis(); auto const cur_mode = this->opentherm_->get_mode(); + + if (this->handle_error_(cur_mode)) { + return; + } + switch (cur_mode) { case OperationMode::WRITE: case OperationMode::READ: case OperationMode::LISTEN: - if (!this->check_timings_(cur_time)) { - break; - } - this->last_mode_ = cur_mode; - break; - case OperationMode::ERROR_PROTOCOL: - if (this->last_mode_ == OperationMode::WRITE) { - this->handle_protocol_write_error_(); - } else if (this->last_mode_ == OperationMode::READ) { - this->handle_protocol_read_error_(); - } - - this->stop_opentherm_(); - break; - case OperationMode::ERROR_TIMEOUT: - this->handle_timeout_error_(); - this->stop_opentherm_(); break; case OperationMode::IDLE: + this->check_timings_(cur_time); if (this->should_skip_loop_(cur_time)) { break; } @@ -219,6 +220,28 @@ void OpenthermHub::loop() { case OperationMode::RECEIVED: this->read_response_(); break; + default: + break; + } + this->last_mode_ = cur_mode; +} + +bool OpenthermHub::handle_error_(OperationMode mode) { + switch (mode) { + case OperationMode::ERROR_PROTOCOL: + // Protocol error can happen only while reading boiler response. + this->handle_protocol_error_(); + return true; + case OperationMode::ERROR_TIMEOUT: + // Timeout error might happen while we wait for device to respond. + this->handle_timeout_error_(); + return true; + case OperationMode::ERROR_TIMER: + // Timer error can happen only on ESP32. + this->handle_timer_error_(); + return true; + default: + return false; } } @@ -237,16 +260,20 @@ void OpenthermHub::sync_loop_() { } this->start_conversation_(); + // There may be a timer error at this point + if (this->handle_error_(this->opentherm_->get_mode())) { + return; + } + // Spin while message is being sent to device if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { ESP_LOGE(TAG, "Hub timeout triggered during send"); this->stop_opentherm_(); return; } - if (this->opentherm_->is_error()) { - this->handle_protocol_write_error_(); - this->stop_opentherm_(); + // Check for errors and ensure we are in the right state (message sent successfully) + if (this->handle_error_(this->opentherm_->get_mode())) { return; } else if (!this->opentherm_->is_sent()) { ESP_LOGW(TAG, "Unexpected state after sending request: %s", @@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() { // Listen for the response this->opentherm_->listen(); + // There may be a timer error at this point + if (this->handle_error_(this->opentherm_->get_mode())) { + return; + } + + // Spin while response is being received if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { ESP_LOGE(TAG, "Hub timeout triggered during receive"); this->stop_opentherm_(); return; } - if (this->opentherm_->is_timeout()) { - this->handle_timeout_error_(); - this->stop_opentherm_(); - return; - } else if (this->opentherm_->is_protocol_error()) { - this->handle_protocol_read_error_(); - this->stop_opentherm_(); + // Check for errors and ensure we are in the right state (message received successfully) + if (this->handle_error_(this->opentherm_->get_mode())) { return; } else if (!this->opentherm_->has_message()) { ESP_LOGW(TAG, "Unexpected state after receiving response: %s", @@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() { this->read_response_(); } -bool OpenthermHub::check_timings_(uint32_t cur_time) { +void OpenthermHub::check_timings_(uint32_t cur_time) { if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { ESP_LOGW(TAG, "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " "components that might slow the loop down.", (int) (cur_time - this->last_conversation_start_)); - this->stop_opentherm_(); - return false; } - - return true; } bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { @@ -304,14 +328,17 @@ bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { } void OpenthermHub::start_conversation_() { - if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { - this->sending_initial_ = false; - this->current_message_iterator_ = this->repeating_messages_.begin(); - } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { - this->current_message_iterator_ = this->repeating_messages_.begin(); + if (this->message_iterator_ == this->messages_.end()) { + if (this->sending_initial_) { + this->sending_initial_ = false; + this->write_repeating_messages_(this->messages_); + } + this->message_iterator_ = this->messages_.begin(); } - auto request = this->build_request_(*this->current_message_iterator_); + auto request = this->build_request_(*this->message_iterator_); + + this->before_send_callback_.call(request); ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, this->opentherm_->message_id_to_str((MessageId) request.id)); @@ -331,37 +358,48 @@ void OpenthermHub::read_response_() { this->stop_opentherm_(); + this->before_process_response_callback_.call(response); this->process_response(response); - this->current_message_iterator_++; + this->message_iterator_++; } void OpenthermHub::stop_opentherm_() { this->opentherm_->stop(); this->last_conversation_end_ = millis(); } -void OpenthermHub::handle_protocol_write_error_() { - ESP_LOGW(TAG, "Error while sending request: %s", - this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); - this->opentherm_->debug_data(this->last_request_); -} -void OpenthermHub::handle_protocol_read_error_() { + +void OpenthermHub::handle_protocol_error_() { OpenThermError error; this->opentherm_->get_protocol_error(error); ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", - this->opentherm_->protocol_error_to_to_str(error.error_type)); + this->opentherm_->protocol_error_to_str(error.error_type)); this->opentherm_->debug_error(error); -} -void OpenthermHub::handle_timeout_error_() { - ESP_LOGW(TAG, "Receive response timed out at a protocol level"); this->stop_opentherm_(); } +void OpenthermHub::handle_timeout_error_() { + ESP_LOGW(TAG, "Timeout while waiting for response from device"); + this->stop_opentherm_(); +} + +void OpenthermHub::handle_timer_error_() { + this->opentherm_->report_and_reset_timer_error(); + this->stop_opentherm_(); + // Timer error is critical, there is no point in retrying. + this->mark_failed(); +} + void OpenthermHub::dump_config() { + std::vector initial_messages; + std::vector repeating_messages; + this->write_initial_messages_(initial_messages); + this->write_repeating_messages_(repeating_messages); + ESP_LOGCONFIG(TAG, "OpenTherm:"); LOG_PIN(" In: ", this->in_pin_); LOG_PIN(" Out: ", this->out_pin_); - ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); + ESP_LOGCONFIG(TAG, " Sync mode: %s", YESNO(this->sync_mode_)); ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); @@ -369,12 +407,12 @@ void OpenthermHub::dump_config() { ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Initial requests:"); - for (auto type : this->initial_messages_) { - ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); + for (auto type : initial_messages) { + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type)); } ESP_LOGCONFIG(TAG, " Repeating requests:"); - for (auto type : this->repeating_messages_) { - ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); + for (auto type : repeating_messages) { + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type)); } } diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 1f536653e8..80fd268820 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -38,6 +38,9 @@ namespace esphome { namespace opentherm { +static const uint8_t REPEATING_MESSAGE_ORDER = 255; +static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254; + // OpenTherm component for ESPHome class OpenthermHub : public Component { protected: @@ -58,15 +61,12 @@ class OpenthermHub : public Component { OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) - // The set of initial messages to send on starting communication with the boiler - std::vector initial_messages_; - // and the repeating messages which are sent repeatedly to update various sensors - // and boiler parameters (like the setpoint). - std::vector repeating_messages_; - // Indicates if we are still working on the initial requests or not + OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, ) + bool sending_initial_ = true; - // Index for the current request in one of the _requests sets. - std::vector::const_iterator current_message_iterator_; + std::unordered_map configured_messages_; + std::vector messages_; + std::vector::const_iterator message_iterator_; uint32_t last_conversation_start_ = 0; uint32_t last_conversation_end_ = 0; @@ -78,20 +78,25 @@ class OpenthermHub : public Component { // Very likely to happen while using Dallas temperature sensors. bool sync_mode_ = false; - float opentherm_version_ = 0.0f; + CallbackManager before_send_callback_; + CallbackManager before_process_response_callback_; // Create OpenTherm messages based on the message id OpenthermData build_request_(MessageId request_id) const; - void handle_protocol_write_error_(); - void handle_protocol_read_error_(); + bool handle_error_(OperationMode mode); + void handle_protocol_error_(); void handle_timeout_error_(); + void handle_timer_error_(); void stop_opentherm_(); void start_conversation_(); void read_response_(); - bool check_timings_(uint32_t cur_time); + void check_timings_(uint32_t cur_time); bool should_skip_loop_(uint32_t cur_time) const; void sync_loop_(); + void write_initial_messages_(std::vector &target); + void write_repeating_messages_(std::vector &target); + template bool spin_wait_(uint32_t timeout, F func) { auto start_time = millis(); while (func()) { @@ -127,13 +132,18 @@ class OpenthermHub : public Component { OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) + OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, ) + // Add a request to the vector of initial requests - void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } + void add_initial_message(MessageId message_id) { + this->configured_messages_[message_id] = INITIAL_UNORDERED_MESSAGE_ORDER; + } + void add_initial_message(MessageId message_id, uint8_t order) { this->configured_messages_[message_id] = order; } // Add a request to the set of repeating requests. Note that a large number of repeating // requests will slow down communication with the boiler. Each request may take up to 1 second, // so with all sensors enabled, it may take about half a minute before a change in setpoint // will be processed. - void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } + void add_repeating_message(MessageId message_id) { this->configured_messages_[message_id] = REPEATING_MESSAGE_ORDER; } // There are seven status variables, which can either be set as a simple variable, // or using a switch. ch_enable and dhw_enable default to true, the others to false. @@ -149,7 +159,13 @@ class OpenthermHub : public Component { void set_summer_mode_active(bool value) { this->summer_mode_active = value; } void set_dhw_block(bool value) { this->dhw_block = value; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } - void set_opentherm_version(float value) { this->opentherm_version_ = value; } + + void add_on_before_send_callback(std::function &&callback) { + this->before_send_callback_.add(std::move(callback)); + } + void add_on_before_process_response_callback(std::function &&callback) { + this->before_process_response_callback_.add(std::move(callback)); + } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 62ab1d3860..49482316ee 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -52,7 +52,9 @@ bool OpenTherm::initialize() { OpenTherm::instance = this; #endif this->in_pin_->pin_mode(gpio::FLAG_INPUT); + this->in_pin_->setup(); this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->out_pin_->setup(); this->out_pin_->digital_write(true); #if defined(ESP32) || defined(USE_ESP_IDF) @@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { } arg->capture_ = 1; // reset counter } else if (arg->capture_ > 0xFF) { - // no change for too long, invalid mancheter encoding + // no change for too long, invalid manchester encoding arg->mode_ = OperationMode::ERROR_PROTOCOL; arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; arg->stop_timer_(); @@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() { } void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { - esp_err_t result; + // We will report timer errors outside of interrupt handler + this->timer_error_ = ESP_OK; + this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); + this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR; + return; + } + this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR; + } +} + +void OpenTherm::report_and_reset_timer_error() { + if (this->timer_error_ == ESP_OK) { return; } - result = timer_start(this->timer_group_, this->timer_idx_); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); - return; - } + ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_), + esp_err_to_name(this->timer_error_)); + + this->timer_error_ = ESP_OK; + this->timer_error_type_ = NO_TIMER_ERROR; } // 5 kHz timer_ @@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() { void IRAM_ATTR OpenTherm::stop_timer_() { InterruptLock const lock; + // We will report timer errors outside of interrupt handler + this->timer_error_ = ESP_OK; + this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - esp_err_t result; - - result = timer_pause(this->timer_group_, this->timer_idx_); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); + this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR; return; } - - result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); - return; + this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR; } } @@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() { timer1_detachInterrupt(); } +// There is nothing to report on ESP8266 +void OpenTherm::report_and_reset_timer_error() {} + #endif // END ESP8266 // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd @@ -412,11 +424,12 @@ const char *OpenTherm::operation_mode_to_str(OperationMode mode) { TO_STRING_MEMBER(SENT) TO_STRING_MEMBER(ERROR_PROTOCOL) TO_STRING_MEMBER(ERROR_TIMEOUT) + TO_STRING_MEMBER(ERROR_TIMER) default: return ""; } } -const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { +const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) { switch (error_type) { TO_STRING_MEMBER(NO_ERROR) TO_STRING_MEMBER(NO_TRANSITION) @@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { return ""; } } +const char *OpenTherm::timer_error_to_str(TimerErrorType error_type) { + switch (error_type) { + TO_STRING_MEMBER(NO_TIMER_ERROR) + TO_STRING_MEMBER(SET_ALARM_VALUE_ERROR) + TO_STRING_MEMBER(TIMER_START_ERROR) + TO_STRING_MEMBER(TIMER_PAUSE_ERROR) + TO_STRING_MEMBER(SET_COUNTER_VALUE_ERROR) + default: + return ""; + } +} const char *OpenTherm::message_type_to_str(MessageType message_type) { switch (message_type) { TO_STRING_MEMBER(READ_DATA) diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3be0191c63..4280832d09 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -36,11 +36,12 @@ enum OperationMode { READ = 2, // reading 32-bit data frame RECEIVED = 3, // data frame received with valid start and stop bit - WRITE = 4, // writing data with timer_ + WRITE = 4, // writing data to output SENT = 5, // all data written to output - ERROR_PROTOCOL = 8, // manchester protocol data transfer error - ERROR_TIMEOUT = 9 // read timeout + ERROR_PROTOCOL = 8, // protocol error, can happed only during READ + ERROR_TIMEOUT = 9, // timeout while waiting for response from device, only during LISTEN + ERROR_TIMER = 10 // error operating the ESP32 timer }; enum ProtocolErrorType { @@ -51,6 +52,14 @@ enum ProtocolErrorType { NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks }; +enum TimerErrorType { + NO_TIMER_ERROR = 0, // No error + SET_ALARM_VALUE_ERROR = 1, // No transition in the middle of the bit + TIMER_START_ERROR = 2, // Stop bit wasn't present when expected + TIMER_PAUSE_ERROR = 3, // Parity check didn't pass + SET_COUNTER_VALUE_ERROR = 4, // No level change for too much timer ticks +}; + enum MessageType { READ_DATA = 0, READ_ACK = 4, @@ -299,7 +308,9 @@ class OpenTherm { * * @return true if last listen() or send() operation ends up with an error. */ - bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } + bool is_error() { + return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL || mode_ == ERROR_TIMER; + } /** * Indicates whether last listen() or send() operation ends up with a *timeout* error @@ -313,14 +324,22 @@ class OpenTherm { */ bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } + /** + * Indicates whether start_esp32_timer_() or stop_timer_() had an error. Only relevant when used on ESP32. + * @return true if there was an error. + */ + bool is_timer_error() { return mode_ == OperationMode::ERROR_TIMER; } + bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } OperationMode get_mode() { return mode_; } void debug_data(OpenthermData &data); void debug_error(OpenThermError &error) const; + void report_and_reset_timer_error(); - const char *protocol_error_to_to_str(ProtocolErrorType error_type); + const char *protocol_error_to_str(ProtocolErrorType error_type); + const char *timer_error_to_str(TimerErrorType error_type); const char *message_type_to_str(MessageType message_type); const char *operation_mode_to_str(OperationMode mode); const char *message_id_to_str(MessageId id); @@ -349,10 +368,12 @@ class OpenTherm { uint32_t data_; uint8_t bit_pos_; int32_t timeout_counter_; // <0 no timeout - int32_t device_timeout_; #if defined(ESP32) || defined(USE_ESP_IDF) + esp_err_t timer_error_ = ESP_OK; + TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; + bool init_esp32_timer_(); void start_esp32_timer_(uint64_t alarm_value); #endif diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h index 8aaec0b48a..398c64aa8f 100644 --- a/esphome/components/opentherm/opentherm_macros.h +++ b/esphome/components/opentherm/opentherm_macros.h @@ -28,6 +28,9 @@ namespace opentherm { #ifndef OPENTHERM_INPUT_SENSOR_LIST #define OPENTHERM_INPUT_SENSOR_LIST(F, sep) #endif +#ifndef OPENTHERM_SETTING_LIST +#define OPENTHERM_SETTING_LIST(F, sep) +#endif // Use macros to create fields for every entity specified in the ESPHome configuration #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; @@ -36,6 +39,7 @@ namespace opentherm { #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; +#define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def; // Setter macros #define OPENTHERM_SET_SENSOR(entity) \ @@ -56,6 +60,9 @@ namespace opentherm { #define OPENTHERM_SET_INPUT_SENSOR(entity) \ void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } +#define OPENTHERM_SET_SETTING(type, entity, def) \ + void set_##entity(type value) { this->entity = value; } + // ===== hub.cpp macros ===== // *_MESSAGE_HANDLERS are generated in defines.h and look like this: @@ -85,6 +92,9 @@ namespace opentherm { #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) #endif +#ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS +#define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif // Write data request builders #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ @@ -92,6 +102,7 @@ namespace opentherm { data.type = MessageType::WRITE_DATA; \ data.id = request_id; #define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); +#define OPENTHERM_MESSAGE_WRITE_SETTING(key, msg_data) message_data::write_##msg_data(this->key, data); #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ return data; \ } diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py index fe0f2a77a3..a58de8e2da 100644 --- a/esphome/components/opentherm/schema.py +++ b/esphome/components/opentherm/schema.py @@ -2,8 +2,9 @@ # inputs of the OpenTherm component. from dataclasses import dataclass -from typing import Optional, TypeVar +from typing import Optional, TypeVar, Any +import esphome.config_validation as cv from esphome.const import ( UNIT_CELSIUS, UNIT_EMPTY, @@ -64,6 +65,7 @@ class SensorSchema(EntitySchema): icon: Optional[str] = None device_class: Optional[str] = None disabled_by_default: bool = False + order: Optional[int] = None SENSORS: dict[str, SensorSchema] = { @@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = { message="OT_VERSION_DEVICE", keep_updated=False, message_data="f88", + order=2, ), "device_type": SensorSchema( description="Device product type", @@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = { message="VERSION_DEVICE", keep_updated=False, message_data="u8_hb", + order=0, ), "device_version": SensorSchema( description="Device product version", @@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = { message="VERSION_DEVICE", keep_updated=False, message_data="u8_lb", + order=0, ), "device_id": SensorSchema( description="Device ID code", @@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = { message="DEVICE_CONFIG", keep_updated=False, message_data="u8_lb", + order=4, ), "otc_hc_ratio_ub": SensorSchema( description="OTC heat curve ratio upper bound", @@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = { class BinarySensorSchema(EntitySchema): icon: Optional[str] = None device_class: Optional[str] = None + order: Optional[int] = None BINARY_SENSORS: dict[str, BinarySensorSchema] = { @@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = { message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_0", + order=4, ), "control_type_on_off": BinarySensorSchema( description="Configuration: Control type is on/off", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_1", + order=4, ), "cooling_supported": BinarySensorSchema( description="Configuration: Cooling supported", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_2", + order=4, ), "dhw_storage_tank": BinarySensorSchema( description="Configuration: DHW storage tank", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_3", + order=4, ), "controller_pump_control_allowed": BinarySensorSchema( description="Configuration: Controller pump control allowed", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_4", + order=4, ), "ch2_present": BinarySensorSchema( description="Configuration: CH2 present", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_5", + order=4, ), "water_filling": BinarySensorSchema( description="Configuration: Remote water filling", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_6", + order=4, ), "heat_mode": BinarySensorSchema( description="Configuration: Heating or cooling", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_7", + order=4, ), "dhw_setpoint_transfer_enabled": BinarySensorSchema( description="Remote boiler parameters: DHW setpoint transfer enabled", @@ -812,3 +827,65 @@ INPUTS: dict[str, InputSchema] = { auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), ), } + + +@dataclass +class SettingSchema(EntitySchema): + backing_type: str + validation_schema: cv.Schema + default_value: Any + order: Optional[int] = None + + +SETTINGS: dict[str, SettingSchema] = { + "controller_product_type": SettingSchema( + description="Controller product type", + message="VERSION_CONTROLLER", + keep_updated=False, + message_data="u8_hb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=1, + ), + "controller_product_version": SettingSchema( + description="Controller product version", + message="VERSION_CONTROLLER", + keep_updated=False, + message_data="u8_lb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=1, + ), + "opentherm_version_controller": SettingSchema( + description="Version of OpenTherm implemented by controller", + message="OT_VERSION_CONTROLLER", + keep_updated=False, + message_data="f88", + backing_type="float", + validation_schema=cv.positive_float, + default_value=0, + order=3, + ), + "controller_configuration": SettingSchema( + description="Controller configuration", + message="CONTROLLER_CONFIG", + keep_updated=False, + message_data="u8_hb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=5, + ), + "controller_id": SettingSchema( + description="Controller ID code", + message="CONTROLLER_CONFIG", + keep_updated=False, + message_data="u8_lb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=5, + ), +} diff --git a/esphome/components/opentherm/validate.py b/esphome/components/opentherm/validate.py index d4507672a5..055cbfa827 100644 --- a/esphome/components/opentherm/validate.py +++ b/esphome/components/opentherm/validate.py @@ -9,12 +9,17 @@ from .schema import TSchema def create_entities_schema( - entities: dict[str, schema.EntitySchema], + entities: dict[str, TSchema], get_entity_validation_schema: Callable[[TSchema], cv.Schema], ) -> Schema: entity_schema = {} for key, entity in entities.items(): - entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity) + schema_key = ( + cv.Optional(key, entity.default_value) + if hasattr(entity, "default_value") + else cv.Optional(key) + ) + entity_schema[schema_key] = get_entity_validation_schema(entity) return cv.Schema(entity_schema) 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/psram/psram.cpp b/esphome/components/psram/psram.cpp index 68d8dfd697..d9a5bd101f 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -21,7 +21,14 @@ void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) if (available) { - ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024); + const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + const float psram_total_size_kb = psram_total_size_bytes / 1024.0f; + + if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) { + ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb); + } else { + ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes); + } } #endif } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index bd3e4fcbef..2bc80c352c 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -13,9 +13,9 @@ PulseCounterStorageBase *get_storage(bool hw_pcnt) { return (hw_pcnt ? (PulseCounterStorageBase *) (new HwPulseCounterStorage) : (PulseCounterStorageBase *) (new BasicPulseCounterStorage)); } -#else +#else // HAS_PCNT PulseCounterStorageBase *get_storage(bool) { return new BasicPulseCounterStorage; } -#endif +#endif // HAS_PCNT void IRAM_ATTR BasicPulseCounterStorage::gpio_intr(BasicPulseCounterStorage *arg) { const uint32_t now = micros(); @@ -28,14 +28,17 @@ void IRAM_ATTR BasicPulseCounterStorage::gpio_intr(BasicPulseCounterStorage *arg switch (mode) { case PULSE_COUNTER_DISABLE: break; - case PULSE_COUNTER_INCREMENT: - arg->counter++; - break; - case PULSE_COUNTER_DECREMENT: - arg->counter--; - break; + case PULSE_COUNTER_INCREMENT: { + auto x = arg->counter + 1; + arg->counter = x; + } break; + case PULSE_COUNTER_DECREMENT: { + auto x = arg->counter - 1; + arg->counter = x; + } break; } } + bool BasicPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { this->pin = pin; this->pin->setup(); @@ -43,6 +46,7 @@ bool BasicPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { this->pin->attach_interrupt(BasicPulseCounterStorage::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); return true; } + pulse_counter_t BasicPulseCounterStorage::read_raw_value() { pulse_counter_t counter = this->counter; pulse_counter_t ret = counter - this->last_value; @@ -141,6 +145,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { } return true; } + pulse_counter_t HwPulseCounterStorage::read_raw_value() { pulse_counter_t counter; pcnt_get_counter_value(this->pcnt_unit, &counter); @@ -148,7 +153,7 @@ pulse_counter_t HwPulseCounterStorage::read_raw_value() { this->last_value = counter; return ret; } -#endif +#endif // HAS_PCNT void PulseCounterSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up pulse counter '%s'...", this->name_.c_str()); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index fc3d8711d1..cea9fa7bf9 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -9,7 +9,7 @@ #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include #define HAS_PCNT -#endif +#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) namespace esphome { namespace pulse_counter { @@ -22,9 +22,9 @@ enum PulseCounterCountMode { #ifdef HAS_PCNT using pulse_counter_t = int16_t; -#else +#else // HAS_PCNT using pulse_counter_t = int32_t; -#endif +#endif // HAS_PCNT struct PulseCounterStorageBase { virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0; @@ -57,7 +57,7 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase { pcnt_unit_t pcnt_unit; pcnt_channel_t pcnt_channel; }; -#endif +#endif // HAS_PCNT PulseCounterStorageBase *get_storage(bool hw_pcnt = false); diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 49a67d4e09..36286244fb 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -81,16 +81,39 @@ void QMC5883LComponent::dump_config() { } float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void QMC5883LComponent::update() { + i2c::ErrorCode err; uint8_t status = false; - this->read_byte(QMC5883L_REGISTER_STATUS, &status); + // Status byte gets cleared when data is read, so we have to read this first. + // If status and two axes are desired, it's possible to save one byte of traffic by enabling + // ROL_PNT in setup and reading 7 bytes starting at the status register. + // If status and all three axes are desired, using ROL_PNT saves you 3 bytes. + // But simply not reading status saves you 4 bytes always and is much simpler. + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) { + err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("status read failed (%d)", err).c_str()); + return; + } + } - // Always request X,Y,Z regardless if there are sensors for them - // to avoid https://github.com/esphome/issues/issues/5731 - uint16_t raw_x, raw_y, raw_z; - if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) || - !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) || - !this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) { - this->status_set_warning(); + uint16_t raw[3] = {0}; + // Z must always be requested, otherwise the data registers will remain locked against updates. + // Skipping the Y axis if X and Z are needed actually requires an additional byte of comms. + // Starting partway through the axes does save you traffic. + uint8_t start, dest; + if (this->heading_sensor_ != nullptr || this->x_sensor_ != nullptr) { + start = QMC5883L_REGISTER_DATA_X_LSB; + dest = 0; + } else if (this->y_sensor_ != nullptr) { + start = QMC5883L_REGISTER_DATA_Y_LSB; + dest = 1; + } else { + start = QMC5883L_REGISTER_DATA_Z_LSB; + dest = 2; + } + err = this->read_bytes_16_le_(start, &raw[dest], 3 - dest); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("mag read failed (%d)", err).c_str()); return; } @@ -107,17 +130,18 @@ void QMC5883LComponent::update() { } // in µT - const float x = int16_t(raw_x) * mg_per_bit * 0.1f; - const float y = int16_t(raw_y) * mg_per_bit * 0.1f; - const float z = int16_t(raw_z) * mg_per_bit * 0.1f; + const float x = int16_t(raw[0]) * mg_per_bit * 0.1f; + const float y = int16_t(raw[1]) * mg_per_bit * 0.1f; + const float z = int16_t(raw[2]) * mg_per_bit * 0.1f; float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; float temp = NAN; if (this->temperature_sensor_ != nullptr) { uint16_t raw_temp; - if (!this->read_byte_16_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp)) { - this->status_set_warning(); + err = this->read_bytes_16_le_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("temp read failed (%d)", err).c_str()); return; } temp = int16_t(raw_temp) * 0.01f; @@ -138,11 +162,13 @@ void QMC5883LComponent::update() { this->temperature_sensor_->publish_state(temp); } -bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { - if (!this->read_byte_16(a_register, data)) - return false; - *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte order, LSB first; - return true; +i2c::ErrorCode QMC5883LComponent::read_bytes_16_le_(uint8_t a_register, uint16_t *data, uint8_t len) { + i2c::ErrorCode err = this->read_register(a_register, reinterpret_cast(data), len * 2); + if (err != i2c::ERROR_OK) + return err; + for (size_t i = 0; i < len; i++) + data[i] = convert_little_endian(data[i]); + return err; } } // namespace qmc5883l diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index dd2008d453..3202e37780 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -55,7 +55,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, } error_code_; - bool read_byte_16_(uint8_t a_register, uint16_t *data); + i2c::ErrorCode read_bytes_16_le_(uint8_t a_register, uint16_t *data, uint8_t len = 1); HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/qspi_dbi/__init__.py b/esphome/components/qspi_dbi/__init__.py index c58ce8a01e..a4b833f6d7 100644 --- a/esphome/components/qspi_dbi/__init__.py +++ b/esphome/components/qspi_dbi/__init__.py @@ -1 +1,4 @@ CODEOWNERS = ["@clydebarrow"] + +CONF_DRAW_FROM_ORIGIN = "draw_from_origin" +CONF_DRAW_ROUNDING = "draw_rounding" diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 71ae31f182..ab6dd66cf2 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -24,6 +24,7 @@ from esphome.const import ( ) from esphome.core import TimePeriod +from . import CONF_DRAW_FROM_ORIGIN, CONF_DRAW_ROUNDING from .models import DriverChip DEPENDENCIES = ["spi"] @@ -41,7 +42,6 @@ COLOR_ORDERS = { } DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema -CONF_DRAW_FROM_ORIGIN = "draw_from_origin" DELAY_FLAG = 0xFF @@ -78,56 +78,81 @@ def _validate(config): return config -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(QSPI_DBI), - cv.Required(CONF_MODEL): cv.one_of( - *DriverChip.chips.keys(), upper=True - ), - cv.Optional(CONF_INIT_SEQUENCE): cv.ensure_list(map_sequence), - cv.Required(CONF_DIMENSIONS): cv.Any( - cv.dimensions, - cv.Schema( - { - cv.Required(CONF_WIDTH): validate_dimension, - cv.Required(CONF_HEIGHT): validate_dimension, - cv.Optional( - CONF_OFFSET_HEIGHT, default=0 - ): validate_dimension, - cv.Optional( - CONF_OFFSET_WIDTH, default=0 - ): validate_dimension, - } - ), - ), - cv.Optional(CONF_TRANSFORM): cv.Schema( +def power_of_two(value): + value = cv.int_range(1, 128)(value) + if value & (value - 1) != 0: + raise cv.Invalid("value must be a power of two") + return value + + +BASE_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QSPI_DBI), + cv.Optional(CONF_INIT_SEQUENCE): cv.ensure_list(map_sequence), + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( { - cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, - cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Required(CONF_WIDTH): validate_dimension, + cv.Required(CONF_HEIGHT): validate_dimension, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension, + cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension, } ), - cv.Optional(CONF_COLOR_ORDER, default="RGB"): cv.enum( - COLOR_ORDERS, upper=True - ), - cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BRIGHTNESS, default=0xD0): cv.int_range( - 0, 0xFF, min_included=True, max_included=True - ), - cv.Optional(CONF_DRAW_FROM_ORIGIN, default=False): cv.boolean, - } - ).extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=10e6, - quad=True, - ) + ), + cv.Optional(CONF_DRAW_FROM_ORIGIN, default=False): cv.boolean, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=0xD0): cv.int_range( + 0, 0xFF, min_included=True, max_included=True + ), + } + ).extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=10e6, + quad=True, ) + ) +) + + +def model_property(name, defaults, fallback): + return cv.Optional(name, default=defaults.get(name, fallback)) + + +def model_schema(defaults): + transform = cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ) + if defaults.get(CONF_SWAP_XY, True): + transform = transform.extend( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + } + ) + return BASE_SCHEMA.extend( + { + model_property(CONF_INVERT_COLORS, defaults, False): cv.boolean, + model_property(CONF_COLOR_ORDER, defaults, "RGB"): cv.enum( + COLOR_ORDERS, upper=True + ), + model_property(CONF_DRAW_ROUNDING, defaults, 2): power_of_two, + cv.Optional(CONF_TRANSFORM): transform, + } + ) + + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + {k.upper(): model_schema(v.defaults) for k, v in DriverChip.chips.items()}, + upper=True, + key=CONF_MODEL, ), cv.only_with_esp_idf, ) @@ -152,6 +177,7 @@ async def to_code(config): cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) + cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = await cg.gpio_pin_expression(enable_pin) cg.add(var.set_enable_pin(enable)) @@ -163,7 +189,8 @@ async def to_code(config): if transform := config.get(CONF_TRANSFORM): cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) - cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + # swap_xy is not implemented for some chips + cg.add(var.set_swap_xy(transform.get(CONF_SWAP_XY, False))) if CONF_DIMENSIONS in config: dimensions = config[CONF_DIMENSIONS] diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py index c1fe434853..7ae1a10ec0 100644 --- a/esphome/components/qspi_dbi/models.py +++ b/esphome/components/qspi_dbi/models.py @@ -1,5 +1,10 @@ # Commands +from esphome.const import CONF_INVERT_COLORS, CONF_SWAP_XY + +from . import CONF_DRAW_ROUNDING + SW_RESET_CMD = 0x01 +SLEEP_IN = 0x10 SLEEP_OUT = 0x11 NORON = 0x13 INVERT_OFF = 0x20 @@ -24,11 +29,12 @@ PAGESEL = 0xFE class DriverChip: chips = {} - def __init__(self, name: str): + def __init__(self, name: str, defaults=None): name = name.upper() self.name = name self.chips[name] = self self.initsequence = [] + self.defaults = defaults or {} def cmd(self, c, *args): """ @@ -59,9 +65,246 @@ chip.cmd(TEON, 0x00) chip.cmd(PIXFMT, 0x55) chip.cmd(NORON) -chip = DriverChip("AXS15231") +chip = DriverChip("AXS15231", {CONF_DRAW_ROUNDING: 8, CONF_SWAP_XY: False}) chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5) chip.cmd(0xC1, 0x33) chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) +chip = DriverChip( + "JC4832W535", + { + CONF_DRAW_ROUNDING: 8, + CONF_SWAP_XY: False, + }, +) +chip.cmd(DISPLAY_OFF) +chip.delay(20) +chip.cmd(SLEEP_IN) +chip.delay(80) +chip.cmd(SLEEP_OUT) +chip.cmd(INVERT_OFF) +# A magic sequence to enable the windowed drawing mode +chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5) +chip.cmd(0xC1, 0x33) +chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + +chip = DriverChip("JC3636W518", {CONF_INVERT_COLORS: True}) +chip.cmd(0xF0, 0x08) +chip.cmd(0xF2, 0x08) +chip.cmd(0x9B, 0x51) +chip.cmd(0x86, 0x53) +chip.cmd(0xF2, 0x80) +chip.cmd(0xF0, 0x00) +chip.cmd(0xF0, 0x01) +chip.cmd(0xF1, 0x01) +chip.cmd(0xB0, 0x54) +chip.cmd(0xB1, 0x3F) +chip.cmd(0xB2, 0x2A) +chip.cmd(0xB4, 0x46) +chip.cmd(0xB5, 0x34) +chip.cmd(0xB6, 0xD5) +chip.cmd(0xB7, 0x30) +chip.cmd(0xBA, 0x00) +chip.cmd(0xBB, 0x08) +chip.cmd(0xBC, 0x08) +chip.cmd(0xBD, 0x00) +chip.cmd(0xC0, 0x80) +chip.cmd(0xC1, 0x10) +chip.cmd(0xC2, 0x37) +chip.cmd(0xC3, 0x80) +chip.cmd(0xC4, 0x10) +chip.cmd(0xC5, 0x37) +chip.cmd(0xC6, 0xA9) +chip.cmd(0xC7, 0x41) +chip.cmd(0xC8, 0x51) +chip.cmd(0xC9, 0xA9) +chip.cmd(0xCA, 0x41) +chip.cmd(0xCB, 0x51) +chip.cmd(0xD0, 0x91) +chip.cmd(0xD1, 0x68) +chip.cmd(0xD2, 0x69) +chip.cmd(0xF5, 0x00, 0xA5) +chip.cmd(0xDD, 0x3F) +chip.cmd(0xDE, 0x3F) +chip.cmd(0xF1, 0x10) +chip.cmd(0xF0, 0x00) +chip.cmd(0xF0, 0x02) +chip.cmd( + 0xE0, + 0x70, + 0x09, + 0x12, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x54, + 0x4E, + 0x19, + 0x15, + 0x15, + 0x2C, + 0x2F, +) +chip.cmd( + 0xE1, + 0x70, + 0x08, + 0x11, + 0x0C, + 0x0B, + 0x27, + 0x38, + 0x43, + 0x4C, + 0x18, + 0x14, + 0x14, + 0x2B, + 0x2D, +) +chip.cmd(0xF0, 0x10) +chip.cmd(0xF3, 0x10) +chip.cmd(0xE0, 0x08) +chip.cmd(0xE1, 0x00) +chip.cmd(0xE2, 0x00) +chip.cmd(0xE3, 0x00) +chip.cmd(0xE4, 0xE0) +chip.cmd(0xE5, 0x06) +chip.cmd(0xE6, 0x21) +chip.cmd(0xE7, 0x00) +chip.cmd(0xE8, 0x05) +chip.cmd(0xE9, 0x82) +chip.cmd(0xEA, 0xDF) +chip.cmd(0xEB, 0x89) +chip.cmd(0xEC, 0x20) +chip.cmd(0xED, 0x14) +chip.cmd(0xEE, 0xFF) +chip.cmd(0xEF, 0x00) +chip.cmd(0xF8, 0xFF) +chip.cmd(0xF9, 0x00) +chip.cmd(0xFA, 0x00) +chip.cmd(0xFB, 0x30) +chip.cmd(0xFC, 0x00) +chip.cmd(0xFD, 0x00) +chip.cmd(0xFE, 0x00) +chip.cmd(0xFF, 0x00) +chip.cmd(0x60, 0x42) +chip.cmd(0x61, 0xE0) +chip.cmd(0x62, 0x40) +chip.cmd(0x63, 0x40) +chip.cmd(0x64, 0x02) +chip.cmd(0x65, 0x00) +chip.cmd(0x66, 0x40) +chip.cmd(0x67, 0x03) +chip.cmd(0x68, 0x00) +chip.cmd(0x69, 0x00) +chip.cmd(0x6A, 0x00) +chip.cmd(0x6B, 0x00) +chip.cmd(0x70, 0x42) +chip.cmd(0x71, 0xE0) +chip.cmd(0x72, 0x40) +chip.cmd(0x73, 0x40) +chip.cmd(0x74, 0x02) +chip.cmd(0x75, 0x00) +chip.cmd(0x76, 0x40) +chip.cmd(0x77, 0x03) +chip.cmd(0x78, 0x00) +chip.cmd(0x79, 0x00) +chip.cmd(0x7A, 0x00) +chip.cmd(0x7B, 0x00) +chip.cmd(0x80, 0x48) +chip.cmd(0x81, 0x00) +chip.cmd(0x82, 0x05) +chip.cmd(0x83, 0x02) +chip.cmd(0x84, 0xDD) +chip.cmd(0x85, 0x00) +chip.cmd(0x86, 0x00) +chip.cmd(0x87, 0x00) +chip.cmd(0x88, 0x48) +chip.cmd(0x89, 0x00) +chip.cmd(0x8A, 0x07) +chip.cmd(0x8B, 0x02) +chip.cmd(0x8C, 0xDF) +chip.cmd(0x8D, 0x00) +chip.cmd(0x8E, 0x00) +chip.cmd(0x8F, 0x00) +chip.cmd(0x90, 0x48) +chip.cmd(0x91, 0x00) +chip.cmd(0x92, 0x09) +chip.cmd(0x93, 0x02) +chip.cmd(0x94, 0xE1) +chip.cmd(0x95, 0x00) +chip.cmd(0x96, 0x00) +chip.cmd(0x97, 0x00) +chip.cmd(0x98, 0x48) +chip.cmd(0x99, 0x00) +chip.cmd(0x9A, 0x0B) +chip.cmd(0x9B, 0x02) +chip.cmd(0x9C, 0xE3) +chip.cmd(0x9D, 0x00) +chip.cmd(0x9E, 0x00) +chip.cmd(0x9F, 0x00) +chip.cmd(0xA0, 0x48) +chip.cmd(0xA1, 0x00) +chip.cmd(0xA2, 0x04) +chip.cmd(0xA3, 0x02) +chip.cmd(0xA4, 0xDC) +chip.cmd(0xA5, 0x00) +chip.cmd(0xA6, 0x00) +chip.cmd(0xA7, 0x00) +chip.cmd(0xA8, 0x48) +chip.cmd(0xA9, 0x00) +chip.cmd(0xAA, 0x06) +chip.cmd(0xAB, 0x02) +chip.cmd(0xAC, 0xDE) +chip.cmd(0xAD, 0x00) +chip.cmd(0xAE, 0x00) +chip.cmd(0xAF, 0x00) +chip.cmd(0xB0, 0x48) +chip.cmd(0xB1, 0x00) +chip.cmd(0xB2, 0x08) +chip.cmd(0xB3, 0x02) +chip.cmd(0xB4, 0xE0) +chip.cmd(0xB5, 0x00) +chip.cmd(0xB6, 0x00) +chip.cmd(0xB7, 0x00) +chip.cmd(0xB8, 0x48) +chip.cmd(0xB9, 0x00) +chip.cmd(0xBA, 0x0A) +chip.cmd(0xBB, 0x02) +chip.cmd(0xBC, 0xE2) +chip.cmd(0xBD, 0x00) +chip.cmd(0xBE, 0x00) +chip.cmd(0xBF, 0x00) +chip.cmd(0xC0, 0x12) +chip.cmd(0xC1, 0xAA) +chip.cmd(0xC2, 0x65) +chip.cmd(0xC3, 0x74) +chip.cmd(0xC4, 0x47) +chip.cmd(0xC5, 0x56) +chip.cmd(0xC6, 0x00) +chip.cmd(0xC7, 0x88) +chip.cmd(0xC8, 0x99) +chip.cmd(0xC9, 0x33) +chip.cmd(0xD0, 0x21) +chip.cmd(0xD1, 0xAA) +chip.cmd(0xD2, 0x65) +chip.cmd(0xD3, 0x74) +chip.cmd(0xD4, 0x47) +chip.cmd(0xD5, 0x56) +chip.cmd(0xD6, 0x00) +chip.cmd(0xD7, 0x88) +chip.cmd(0xD8, 0x99) +chip.cmd(0xD9, 0x33) +chip.cmd(0xF3, 0x01) +chip.cmd(0xF0, 0x00) +chip.cmd(0xF0, 0x01) +chip.cmd(0xF1, 0x01) +chip.cmd(0xA0, 0x0B) +chip.cmd(0xA3, 0x2A) +chip.cmd(0xA5, 0xC3) +chip.cmd(PIXFMT, 0x55) + + DriverChip("Custom") diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index f8fd5dd374..380c93c400 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -33,19 +33,12 @@ void QspiDbi::update() { this->do_update_(); if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) return; - // Start addresses and widths/heights must be divisible by 2 (CASET/RASET restriction in datasheet) - if (this->x_low_ % 2 == 1) { - this->x_low_--; - } - if (this->x_high_ % 2 == 0) { - this->x_high_++; - } - if (this->y_low_ % 2 == 1) { - this->y_low_--; - } - if (this->y_high_ % 2 == 0) { - this->y_high_++; - } + // Some chips require that the drawing window be aligned on certain boundaries + auto dr = this->draw_rounding_; + this->x_low_ = this->x_low_ / dr * dr; + this->y_low_ = this->y_low_ / dr * dr; + this->x_high_ = (this->x_high_ + dr) / dr * dr - 1; + this->y_high_ = (this->y_high_ + dr) / dr * dr - 1; if (this->draw_from_origin_) { this->x_low_ = 0; this->y_low_ = 0; @@ -175,10 +168,9 @@ void QspiDbi::write_to_display_(int x_start, int y_start, int w, int h, const ui this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, ptr, w * h * 2, 4); } else { auto stride = x_offset + w + x_pad; - uint16_t cmd = 0x2C00; + this->write_cmd_addr_data(8, 0x32, 24, 0x2C00, nullptr, 0, 4); for (int y = 0; y != h; y++) { - this->write_cmd_addr_data(8, 0x32, 24, cmd, ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2, 4); - cmd = 0x3C00; + this->write_cmd_addr_data(0, 0, 0, 0, ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2, 4); } } this->disable(); @@ -220,6 +212,7 @@ void QspiDbi::dump_config() { ESP_LOGCONFIG("", "Model: %s", this->model_); ESP_LOGCONFIG(TAG, " Height: %u", this->height_); ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); diff --git a/esphome/components/qspi_dbi/qspi_dbi.h b/esphome/components/qspi_dbi/qspi_dbi.h index ebb65a8a05..2c555f115e 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.h +++ b/esphome/components/qspi_dbi/qspi_dbi.h @@ -4,12 +4,10 @@ #pragma once #ifdef USE_ESP_IDF -#include "esphome/core/component.h" #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_color_utils.h" -#include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_rgb.h" @@ -105,6 +103,7 @@ class QspiDbi : public display::DisplayBuffer, int get_height_internal() override { return this->height_; } bool can_proceed() override { return this->setup_complete_; } void add_init_sequence(const std::vector &sequence) { this->init_sequences_.push_back(sequence); } + void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } protected: void check_buffer_() { @@ -161,6 +160,7 @@ class QspiDbi : public display::DisplayBuffer, bool mirror_x_{}; bool mirror_y_{}; bool draw_from_origin_{false}; + unsigned draw_rounding_{2}; uint8_t brightness_{0xD0}; const char *model_{"Unknown"}; std::vector> init_sequences_{}; diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index fdfd0b43cc..5dff2c6a38 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -8,7 +8,7 @@ namespace remote_base { static const char *const TAG = "remote_base"; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_block_num) { static rmt_channel_t next_rmt_channel = RMT_CHANNEL_0; this->channel_ = next_rmt_channel; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index c31127735a..70691177ef 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -8,7 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 #include #endif @@ -112,25 +112,43 @@ class RemoteComponentBase { #ifdef USE_ESP32 class RemoteRMTChannel { public: +#if ESP_IDF_VERSION_MAJOR >= 5 + void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } + void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } +#else explicit RemoteRMTChannel(uint8_t mem_block_num = 1); explicit RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num = 1); void config_rmt(rmt_config_t &rmt); void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } +#endif protected: uint32_t from_microseconds_(uint32_t us) { +#if ESP_IDF_VERSION_MAJOR >= 5 + const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; +#else const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; +#endif return us * ticks_per_ten_us / 10; } uint32_t to_microseconds_(uint32_t ticks) { +#if ESP_IDF_VERSION_MAJOR >= 5 + const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; +#else const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; +#endif return (ticks * 10) / ticks_per_ten_us; } RemoteComponentBase *remote_base_; +#if ESP_IDF_VERSION_MAJOR >= 5 + uint32_t clock_resolution_{1000000}; + uint32_t rmt_symbols_; +#else rmt_channel_t channel_{RMT_CHANNEL_0}; uint8_t mem_block_num_; uint8_t clock_divider_{80}; +#endif }; #endif diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index e5085bb33c..b01443a974 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,23 +1,28 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import remote_base, esp32_rmt +import esphome.codegen as cg +from esphome.components import esp32, esp32_rmt, remote_base +import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_CLOCK_DIVIDER, + CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, CONF_ID, CONF_IDLE, + CONF_MEMORY_BLOCKS, CONF_PIN, + CONF_RMT_CHANNEL, + CONF_RMT_SYMBOLS, CONF_TOLERANCE, CONF_TYPE, - CONF_MEMORY_BLOCKS, - CONF_RMT_CHANNEL, + CONF_USE_DMA, CONF_VALUE, ) from esphome.core import CORE, TimePeriod -CONF_CLOCK_DIVIDER = "clock_divider" +CONF_FILTER_SYMBOLS = "filter_symbols" +CONF_RECEIVE_SYMBOLS = "receive_symbols" AUTO_LOAD = ["remote_base"] remote_receiver_ns = cg.esphome_ns.namespace("remote_receiver") @@ -98,15 +103,43 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.SplitDefault(CONF_CLOCK_DIVIDER, esp32=80): cv.All( - cv.only_on_esp32, cv.Range(min=1, max=255) + cv.SplitDefault(CONF_CLOCK_DIVIDER, esp32_arduino=80): cv.All( + cv.only_on_esp32, + cv.only_with_arduino, + cv.int_range(min=1, max=255), + ), + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + cv.only_with_esp_idf, + esp32_rmt.validate_clock_resolution(), ), cv.Optional(CONF_IDLE, default="10ms"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.Optional(CONF_MEMORY_BLOCKS, default=3): cv.Range(min=1, max=8), - cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=False), + cv.SplitDefault(CONF_MEMORY_BLOCKS, esp32_arduino=3): cv.All( + cv.only_with_arduino, cv.int_range(min=1, max=8) + ), + cv.Optional(CONF_RMT_CHANNEL): cv.All( + cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=False) + ), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32_idf=192, + esp32_s2_idf=192, + esp32_s3_idf=192, + esp32_c3_idf=96, + esp32_c6_idf=96, + esp32_h2_idf=96, + ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + cv.Optional(CONF_FILTER_SYMBOLS): cv.All( + cv.only_with_esp_idf, cv.int_range(min=0) + ), + cv.SplitDefault( + CONF_RECEIVE_SYMBOLS, + esp32_idf=192, + ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + cv.Optional(CONF_USE_DMA): cv.All(cv.only_with_esp_idf, cv.boolean), } ).extend(cv.COMPONENT_SCHEMA) ) @@ -115,13 +148,27 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable( - config[CONF_ID], pin, rmt_channel, config[CONF_MEMORY_BLOCKS] - ) + if esp32_rmt.use_new_rmt_driver(): + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_FILTER_SYMBOLS in config: + cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) + if CORE.using_esp_idf: + esp32.add_idf_sdkconfig_option("CONFIG_RMT_RECV_FUNC_IN_IRAM", True) + esp32.add_idf_sdkconfig_option("CONFIG_RMT_ISR_IRAM_SAFE", True) else: - var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) - cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) + if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: + var = cg.new_Pvariable( + config[CONF_ID], pin, rmt_channel, config[CONF_MEMORY_BLOCKS] + ) + else: + var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) + cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) else: var = cg.new_Pvariable(config[CONF_ID], pin) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 773f8cf636..8d19d5490f 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -5,6 +5,10 @@ #include +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#include +#endif + namespace esphome { namespace remote_receiver { @@ -25,6 +29,21 @@ struct RemoteReceiverComponentStore { uint32_t filter_us{10}; ISRInternalGPIOPin pin; }; +#elif defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +struct RemoteReceiverComponentStore { + /// Stores RMT symbols and rx done event data + volatile uint8_t *buffer{nullptr}; + /// The position last written to + volatile uint32_t buffer_write{0}; + /// The position last read from + volatile uint32_t buffer_read{0}; + bool overflow{false}; + uint32_t buffer_size{1000}; + uint32_t receive_size{0}; + uint32_t filter_symbols{0}; + esp_err_t error{ESP_OK}; + rmt_receive_config_t config; +}; #endif class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, @@ -33,9 +52,10 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, , public remote_base::RemoteRMTChannel #endif + { public: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 RemoteReceiverComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} @@ -49,19 +69,32 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, void loop() override; float get_setup_priority() const override { return setup_priority::DATA; } +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 + void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } + void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } + void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } +#endif void set_buffer_size(uint32_t buffer_size) { this->buffer_size_ = buffer_size; } void set_filter_us(uint32_t filter_us) { this->filter_us_ = filter_us; } void set_idle_us(uint32_t idle_us) { this->idle_us_ = idle_us; } protected: #ifdef USE_ESP32 - void decode_rmt_(rmt_item32_t *item, size_t len); +#if ESP_IDF_VERSION_MAJOR >= 5 + void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); + rmt_channel_handle_t channel_{NULL}; + uint32_t filter_symbols_{0}; + uint32_t receive_symbols_{0}; + bool with_dma_{false}; +#else + void decode_rmt_(rmt_item32_t *item, size_t item_count); RingbufHandle_t ringbuf_; +#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || (defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5) RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 91295871e2..8a36971e36 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,15 +2,104 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 -#include namespace esphome { namespace remote_receiver { static const char *const TAG = "remote_receiver.esp32"; +#ifdef USE_ESP32_VARIANT_ESP32H2 +static const uint32_t RMT_CLK_FREQ = 32000000; +#else +static const uint32_t RMT_CLK_FREQ = 80000000; +#endif + +#if ESP_IDF_VERSION_MAJOR >= 5 +static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { + RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; + rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + store->buffer_write); + uint32_t event_size = sizeof(rmt_rx_done_event_data_t); + uint32_t next_write = store->buffer_write + event_size + event->num_symbols * sizeof(rmt_symbol_word_t); + if (next_write + event_size + store->receive_size > store->buffer_size) { + next_write = 0; + } + if (store->buffer_read - next_write < event_size + store->receive_size) { + next_write = store->buffer_write; + store->overflow = true; + } + if (event->num_symbols <= store->filter_symbols) { + next_write = store->buffer_write; + } + store->error = + rmt_receive(channel, (uint8_t *) store->buffer + next_write + event_size, store->receive_size, &store->config); + event_buffer->num_symbols = event->num_symbols; + event_buffer->received_symbols = event->received_symbols; + store->buffer_write = next_write; + return false; +} +#endif void RemoteReceiverComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); +#if ESP_IDF_VERSION_MAJOR >= 5 + rmt_rx_channel_config_t channel; + memset(&channel, 0, sizeof(channel)); + channel.clk_src = RMT_CLK_SRC_DEFAULT; + channel.resolution_hz = this->clock_resolution_; + channel.mem_block_symbols = rmt_symbols_; + channel.gpio_num = gpio_num_t(this->pin_->get_pin()); + channel.intr_priority = 0; + channel.flags.invert_in = 0; + channel.flags.with_dma = this->with_dma_; + channel.flags.io_loop_back = 0; + esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_); + if (error != ESP_OK) { + this->error_code_ = error; + if (error == ESP_ERR_NOT_FOUND) { + this->error_string_ = "out of RMT symbol memory"; + } else { + this->error_string_ = "in rmt_new_rx_channel"; + } + this->mark_failed(); + return; + } + error = rmt_enable(this->channel_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_enable"; + this->mark_failed(); + return; + } + + rmt_rx_event_callbacks_t callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.on_recv_done = rmt_callback; + error = rmt_rx_register_event_callbacks(this->channel_, &callbacks, &this->store_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_rx_register_event_callbacks"; + this->mark_failed(); + return; + } + + uint32_t event_size = sizeof(rmt_rx_done_event_data_t); + uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000); + uint32_t max_idle_ns = 65535u * 1000; + memset(&this->store_.config, 0, sizeof(this->store_.config)); + this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns); + this->store_.config.signal_range_max_ns = std::min(this->idle_us_ * 1000, max_idle_ns); + this->store_.filter_symbols = this->filter_symbols_; + this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t); + this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_); + this->store_.buffer = new uint8_t[this->buffer_size_]; + error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size, + &this->store_.config); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_receive"; + this->mark_failed(); + return; + } +#else this->pin_->setup(); rmt_config_t rmt{}; this->config_rmt(rmt); @@ -59,10 +148,18 @@ void RemoteReceiverComponent::setup() { this->mark_failed(); return; } +#endif } + void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:"); LOG_PIN(" Pin: ", this->pin_); +#if ESP_IDF_VERSION_MAJOR >= 5 + ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz", this->clock_resolution_); + ESP_LOGCONFIG(TAG, " RMT symbols: %" PRIu32, this->rmt_symbols_); + ESP_LOGCONFIG(TAG, " Filter symbols: %" PRIu32, this->filter_symbols_); + ESP_LOGCONFIG(TAG, " Receive symbols: %" PRIu32, this->receive_symbols_); +#else if (this->pin_->digital_read()) { ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " "invert the signal using 'inverted: True' in the pin schema!"); @@ -70,6 +167,7 @@ void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); +#endif ESP_LOGCONFIG(TAG, " Tolerance: %" PRIu32 "%s", this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%"); ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %" PRIu32 " us", this->filter_us_); @@ -81,10 +179,38 @@ void RemoteReceiverComponent::dump_config() { } void RemoteReceiverComponent::loop() { +#if ESP_IDF_VERSION_MAJOR >= 5 + if (this->store_.error != ESP_OK) { + ESP_LOGE(TAG, "Receive error"); + this->error_code_ = this->store_.error; + this->error_string_ = "in rmt_callback"; + this->mark_failed(); + } + if (this->store_.overflow) { + ESP_LOGW(TAG, "Buffer overflow"); + this->store_.overflow = false; + } + uint32_t buffer_write = this->store_.buffer_write; + while (this->store_.buffer_read != buffer_write) { + rmt_rx_done_event_data_t *event = (rmt_rx_done_event_data_t *) (this->store_.buffer + this->store_.buffer_read); + uint32_t event_size = sizeof(rmt_rx_done_event_data_t); + uint32_t next_read = this->store_.buffer_read + event_size + event->num_symbols * sizeof(rmt_symbol_word_t); + if (next_read + event_size + this->store_.receive_size > this->store_.buffer_size) { + next_read = 0; + } + this->decode_rmt_(event->received_symbols, event->num_symbols); + this->store_.buffer_read = next_read; + + if (!this->temp_.empty()) { + this->temp_.push_back(-this->idle_us_); + this->call_listeners_dumpers_(); + } + } +#else size_t len = 0; auto *item = (rmt_item32_t *) xRingbufferReceive(this->ringbuf_, &len, 0); if (item != nullptr) { - this->decode_rmt_(item, len); + this->decode_rmt_(item, len / sizeof(rmt_item32_t)); vRingbufferReturnItem(this->ringbuf_, item); if (this->temp_.empty()) @@ -93,13 +219,18 @@ void RemoteReceiverComponent::loop() { this->temp_.push_back(-this->idle_us_); this->call_listeners_dumpers_(); } +#endif } -void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { + +#if ESP_IDF_VERSION_MAJOR >= 5 +void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_count) { +#else +void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) { +#endif bool prev_level = false; uint32_t prev_length = 0; this->temp_.clear(); int32_t multiplier = this->pin_->is_inverted() ? -1 : 1; - size_t item_count = len / sizeof(rmt_item32_t); uint32_t filter_ticks = this->from_microseconds_(this->filter_us_); ESP_LOGVV(TAG, "START:"); @@ -124,7 +255,8 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { this->temp_.reserve(item_count * 2); // each RMT item has 2 pulses for (size_t i = 0; i < item_count; i++) { if (item[i].duration0 == 0u) { - // Do nothing + // EOF, sometimes garbage follows, break early + break; } else if ((bool(item[i].level0) == prev_level) || (item[i].duration0 < filter_ticks)) { prev_length += item[i].duration0; } else { @@ -140,7 +272,8 @@ void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t len) { } if (item[i].duration1 == 0u) { - // Do nothing + // EOF, sometimes garbage follows, break early + break; } else if ((bool(item[i].level1) == prev_level) || (item[i].duration1 < filter_ticks)) { prev_length += item[i].duration1; } else { diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index f979939739..e3462fb246 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -2,12 +2,25 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32_rmt, remote_base import esphome.config_validation as cv -from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN, CONF_RMT_CHANNEL +from esphome.const import ( + CONF_CARRIER_DUTY_PERCENT, + CONF_CLOCK_DIVIDER, + CONF_CLOCK_RESOLUTION, + CONF_ID, + CONF_INVERTED, + CONF_PIN, + CONF_RMT_CHANNEL, + CONF_RMT_SYMBOLS, + CONF_USE_DMA, +) +from esphome.core import CORE AUTO_LOAD = ["remote_base"] +CONF_EOT_LEVEL = "eot_level" CONF_ON_TRANSMIT = "on_transmit" CONF_ON_COMPLETE = "on_complete" +CONF_ONE_WIRE = "one_wire" remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") RemoteTransmitterComponent = remote_transmitter_ns.class_( @@ -22,7 +35,29 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_CARRIER_DUTY_PERCENT): cv.All( cv.percentage_int, cv.Range(min=1, max=100) ), - cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True), + cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( + cv.only_on_esp32, + cv.only_with_esp_idf, + esp32_rmt.validate_clock_resolution(), + ), + cv.Optional(CONF_CLOCK_DIVIDER): cv.All( + cv.only_on_esp32, cv.only_with_arduino, cv.int_range(min=1, max=255) + ), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_with_esp_idf, cv.boolean), + cv.Optional(CONF_ONE_WIRE): cv.All(cv.only_with_esp_idf, cv.boolean), + cv.Optional(CONF_USE_DMA): cv.All(cv.only_with_esp_idf, cv.boolean), + cv.SplitDefault( + CONF_RMT_SYMBOLS, + esp32_idf=64, + esp32_s2_idf=64, + esp32_s3_idf=48, + esp32_c3_idf=48, + esp32_c6_idf=48, + esp32_h2_idf=48, + ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + cv.Optional(CONF_RMT_CHANNEL): cv.All( + cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True) + ), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } @@ -31,8 +66,30 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) + if CORE.is_esp32: + if esp32_rmt.use_new_rmt_driver(): + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_ONE_WIRE in config: + cg.add(var.set_one_wire(config[CONF_ONE_WIRE])) + if CONF_EOT_LEVEL in config: + cg.add(var.set_eot_level(config[CONF_EOT_LEVEL])) + elif CONF_ONE_WIRE in config and config[CONF_ONE_WIRE]: + cg.add(var.set_eot_level(True)) + elif CONF_INVERTED in config[CONF_PIN] and config[CONF_PIN][CONF_INVERTED]: + cg.add(var.set_eot_level(True)) + else: + if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: + var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) + else: + var = cg.new_Pvariable(config[CONF_ID], pin) + if CONF_CLOCK_DIVIDER in config: + cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) + else: var = cg.new_Pvariable(config[CONF_ID], pin) await cg.register_component(var, config) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 4abe687d23..fd1d182063 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -5,6 +5,10 @@ #include +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#include +#endif + namespace esphome { namespace remote_transmitter { @@ -16,7 +20,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif { public: -#ifdef USE_ESP32 +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 RemoteTransmitterComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} @@ -29,10 +33,18 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } + // transmitter setup must run after receiver setup to allow the same GPIO to be used by both + float get_setup_priority() const override { return setup_priority::DATA - 1; } void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } +#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 + void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } + void set_one_wire(bool one_wire) { this->one_wire_ = one_wire; } + void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } + void digital_write(bool value); +#endif + Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; Trigger<> *get_complete_trigger() const { return this->complete_trigger_; }; @@ -54,7 +66,16 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; +#if ESP_IDF_VERSION_MAJOR >= 5 + std::vector rmt_temp_; + bool with_dma_{false}; + bool one_wire_{false}; + bool eot_level_{false}; + rmt_channel_handle_t channel_{NULL}; + rmt_encoder_handle_t encoder_{NULL}; +#else std::vector rmt_temp_; +#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; bool inverted_{false}; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index bce2408723..cd7f366373 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -9,13 +9,23 @@ namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; -void RemoteTransmitterComponent::setup() { this->configure_rmt_(); } +void RemoteTransmitterComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Remote Transmitter..."); + this->inverted_ = this->pin_->is_inverted(); + this->configure_rmt_(); +} void RemoteTransmitterComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Remote Transmitter..."); + ESP_LOGCONFIG(TAG, "Remote Transmitter:"); +#if ESP_IDF_VERSION_MAJOR >= 5 + ESP_LOGCONFIG(TAG, " One wire: %s", this->one_wire_ ? "true" : "false"); + ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz", this->clock_resolution_); + ESP_LOGCONFIG(TAG, " RMT symbols: %" PRIu32, this->rmt_symbols_); +#else ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); +#endif LOG_PIN(" Pin: ", this->pin_); if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) { @@ -28,7 +38,99 @@ void RemoteTransmitterComponent::dump_config() { } } +#if ESP_IDF_VERSION_MAJOR >= 5 +void RemoteTransmitterComponent::digital_write(bool value) { + rmt_symbol_word_t symbol = { + .duration0 = 1, + .level0 = value, + .duration1 = 0, + .level1 = value, + }; + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.loop_count = 0; + config.flags.eot_level = value; + esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } + error = rmt_tx_wait_all_done(this->channel_, -1); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } +} +#endif + void RemoteTransmitterComponent::configure_rmt_() { +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_err_t error; + + if (!this->initialized_) { + rmt_tx_channel_config_t channel; + memset(&channel, 0, sizeof(channel)); + channel.clk_src = RMT_CLK_SRC_DEFAULT; + channel.resolution_hz = this->clock_resolution_; + channel.gpio_num = gpio_num_t(this->pin_->get_pin()); + channel.mem_block_symbols = this->rmt_symbols_; + channel.trans_queue_depth = 1; + channel.flags.io_loop_back = this->one_wire_; + channel.flags.io_od_mode = this->one_wire_; + channel.flags.invert_out = 0; + channel.flags.with_dma = this->with_dma_; + channel.intr_priority = 0; + error = rmt_new_tx_channel(&channel, &this->channel_); + if (error != ESP_OK) { + this->error_code_ = error; + if (error == ESP_ERR_NOT_FOUND) { + this->error_string_ = "out of RMT symbol memory"; + } else { + this->error_string_ = "in rmt_new_tx_channel"; + } + this->mark_failed(); + return; + } + + rmt_copy_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + error = rmt_new_copy_encoder(&encoder, &this->encoder_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_new_copy_encoder"; + this->mark_failed(); + return; + } + + error = rmt_enable(this->channel_); + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_enable"; + this->mark_failed(); + return; + } + this->digital_write(this->one_wire_ || this->inverted_); + this->initialized_ = true; + } + + if (this->current_carrier_frequency_ == 0 || this->carrier_duty_percent_ == 100) { + error = rmt_apply_carrier(this->channel_, nullptr); + } else { + rmt_carrier_config_t carrier; + memset(&carrier, 0, sizeof(carrier)); + carrier.frequency_hz = this->current_carrier_frequency_; + carrier.duty_cycle = (float) this->carrier_duty_percent_ / 100.0f; + carrier.flags.polarity_active_low = this->inverted_; + carrier.flags.always_on = 1; + error = rmt_apply_carrier(this->channel_, &carrier); + } + if (error != ESP_OK) { + this->error_code_ = error; + this->error_string_ = "in rmt_apply_carrier"; + this->mark_failed(); + return; + } +#else rmt_config_t c{}; this->config_rmt(c); @@ -45,13 +147,12 @@ void RemoteTransmitterComponent::configure_rmt_() { } c.tx_config.idle_output_en = true; - if (!this->pin_->is_inverted()) { + if (!this->inverted_) { c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; } else { c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; - this->inverted_ = true; } esp_err_t error = rmt_config(&c); @@ -76,6 +177,7 @@ void RemoteTransmitterComponent::configure_rmt_() { } this->initialized_ = true; } +#endif } void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { @@ -90,7 +192,11 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->rmt_temp_.clear(); this->rmt_temp_.reserve((this->temp_.get_data().size() + 1) / 2); uint32_t rmt_i = 0; +#if ESP_IDF_VERSION_MAJOR >= 5 + rmt_symbol_word_t rmt_item; +#else rmt_item32_t rmt_item; +#endif for (int32_t val : this->temp_.get_data()) { bool level = val >= 0; @@ -125,6 +231,29 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen return; } this->transmit_trigger_->trigger(); +#if ESP_IDF_VERSION_MAJOR >= 5 + for (uint32_t i = 0; i < send_times; i++) { + rmt_transmit_config_t config; + memset(&config, 0, sizeof(config)); + config.loop_count = 0; + config.flags.eot_level = this->eot_level_; + esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), + this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + error = rmt_tx_wait_all_done(this->channel_, -1); + if (error != ESP_OK) { + ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); + this->status_set_warning(); + } + if (i + 1 < send_times) + delayMicroseconds(send_wait); + } +#else for (uint32_t i = 0; i < send_times; i++) { esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); if (error != ESP_OK) { @@ -136,6 +265,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } +#endif this->complete_trigger_->trigger(); } 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/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index e9a0eac3f5..f8e5357a6e 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -93,13 +93,17 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore int8_t rotation_dir = 0; uint16_t new_state = STATE_LOOKUP_TABLE[input_state]; if ((new_state & arg->resolution & STATE_HAS_INCREMENTED) != 0) { - if (arg->counter < arg->max_value) - arg->counter++; + if (arg->counter < arg->max_value) { + auto x = arg->counter + 1; + arg->counter = x; + } rotation_dir = 1; } if ((new_state & arg->resolution & STATE_HAS_DECREMENTED) != 0) { - if (arg->counter > arg->min_value) - arg->counter--; + if (arg->counter > arg->min_value) { + auto x = arg->counter - 1; + arg->counter = x; + } rotation_dir = -1; } diff --git a/esphome/components/seeed_mr60bha2/__init__.py b/esphome/components/seeed_mr60bha2/__init__.py new file mode 100644 index 0000000000..87bdbbd003 --- /dev/null +++ b/esphome/components/seeed_mr60bha2/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@limengdu"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +mr60bha2_ns = cg.esphome_ns.namespace("seeed_mr60bha2") + +MR60BHA2Component = mr60bha2_ns.class_( + "MR60BHA2Component", cg.Component, uart.UARTDevice +) + +CONF_MR60BHA2_ID = "mr60bha2_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MR60BHA2Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "seeed_mr60bha2", + require_tx=True, + require_rx=True, + baud_rate=115200, + 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/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp new file mode 100644 index 0000000000..50d709c3b0 --- /dev/null +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -0,0 +1,173 @@ +#include "seeed_mr60bha2.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace seeed_mr60bha2 { + +static const char *const TAG = "seeed_mr60bha2"; + +// Prints the component's configuration data. dump_config() prints all of the component's configuration +// items in an easy-to-read format, including the configuration key-value pairs. +void MR60BHA2Component::dump_config() { + ESP_LOGCONFIG(TAG, "MR60BHA2:"); +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Breath Rate Sensor", this->breath_rate_sensor_); + LOG_SENSOR(" ", "Heart Rate Sensor", this->heart_rate_sensor_); + LOG_SENSOR(" ", "Distance Sensor", this->distance_sensor_); +#endif +} + +// main loop +void MR60BHA2Component::loop() { + uint8_t byte; + + // Is there data on the serial port + while (this->available()) { + this->read_byte(&byte); + this->rx_message_.push_back(byte); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } + } +} + +/** + * @brief Calculate the checksum for a byte array. + * + * This function calculates the checksum for the provided byte array using an + * XOR-based checksum algorithm. + * + * @param data The byte array to calculate the checksum for. + * @param len The length of the byte array. + * @return The calculated checksum. + */ +static uint8_t calculate_checksum(const uint8_t *data, size_t len) { + uint8_t checksum = 0; + for (size_t i = 0; i < len; i++) { + checksum ^= data[i]; + } + checksum = ~checksum; + return checksum; +} + +/** + * @brief Validate the checksum of a byte array. + * + * This function validates the checksum of the provided byte array by comparing + * it to the expected checksum. + * + * @param data The byte array to validate. + * @param len The length of the byte array. + * @param expected_checksum The expected checksum. + * @return True if the checksum is valid, false otherwise. + */ +static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_checksum) { + return calculate_checksum(data, len) == expected_checksum; +} + +bool MR60BHA2Component::validate_message_() { + size_t at = this->rx_message_.size() - 1; + auto *data = &this->rx_message_[0]; + uint8_t new_byte = data[at]; + + if (at == 0) { + return new_byte == FRAME_HEADER_BUFFER; + } + + if (at <= 2) { + return true; + } + uint16_t frame_id = encode_uint16(data[1], data[2]); + + if (at <= 4) { + return true; + } + + uint16_t length = encode_uint16(data[3], data[4]); + + if (at <= 6) { + return true; + } + + uint16_t frame_type = encode_uint16(data[5], data[6]); + + if (frame_type != BREATH_RATE_TYPE_BUFFER && frame_type != HEART_RATE_TYPE_BUFFER && + frame_type != DISTANCE_TYPE_BUFFER) { + return false; + } + + uint8_t header_checksum = new_byte; + + if (at == 7) { + if (!validate_checksum(data, 7, header_checksum)) { + ESP_LOGE(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", header_checksum); + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8).c_str()); + return false; + } + return true; + } + + // Wait until all data is read + if (at - 8 < length) { + return true; + } + + uint8_t data_checksum = new_byte; + if (at == 8 + length) { + if (!validate_checksum(data + 8, length, data_checksum)) { + ESP_LOGE(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", data_checksum); + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8 + length).c_str()); + return false; + } + } + + const uint8_t *frame_data = data + 8; + ESP_LOGV(TAG, "Received Frame: ID: 0x%04x, Type: 0x%04x, Data: [%s] Raw Data: [%s]", frame_id, frame_type, + format_hex_pretty(frame_data, length).c_str(), format_hex_pretty(this->rx_message_).c_str()); + this->process_frame_(frame_id, frame_type, data + 8, length); + + // Return false to reset rx buffer + return false; +} + +void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length) { + switch (frame_type) { + case BREATH_RATE_TYPE_BUFFER: + if (this->breath_rate_sensor_ != nullptr && length >= 4) { + uint32_t current_breath_rate_int = encode_uint32(data[3], data[2], data[1], data[0]); + if (current_breath_rate_int != 0) { + float breath_rate_float; + memcpy(&breath_rate_float, ¤t_breath_rate_int, sizeof(float)); + this->breath_rate_sensor_->publish_state(breath_rate_float); + } + } + break; + case HEART_RATE_TYPE_BUFFER: + if (this->heart_rate_sensor_ != nullptr && length >= 4) { + uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]); + if (current_heart_rate_int != 0) { + float heart_rate_float; + memcpy(&heart_rate_float, ¤t_heart_rate_int, sizeof(float)); + this->heart_rate_sensor_->publish_state(heart_rate_float); + } + } + break; + case DISTANCE_TYPE_BUFFER: + if (!data[0]) { + if (this->distance_sensor_ != nullptr && length >= 8) { + uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]); + float distance_float; + memcpy(&distance_float, ¤t_distance_int, sizeof(float)); + this->distance_sensor_->publish_state(distance_float); + } + } + break; + default: + break; + } +} + +} // namespace seeed_mr60bha2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h new file mode 100644 index 0000000000..0a4f21f1ad --- /dev/null +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h @@ -0,0 +1,61 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace seeed_mr60bha2 { + +static const uint8_t DATA_BUF_MAX_SIZE = 12; +static const uint8_t FRAME_BUF_MAX_SIZE = 21; +static const uint8_t LEN_TO_HEAD_CKSUM = 8; +static const uint8_t LEN_TO_DATA_FRAME = 9; + +static const uint8_t FRAME_HEADER_BUFFER = 0x01; +static const uint16_t BREATH_RATE_TYPE_BUFFER = 0x0A14; +static const uint16_t HEART_RATE_TYPE_BUFFER = 0x0A15; +static const uint16_t DISTANCE_TYPE_BUFFER = 0x0A16; + +enum FrameLocation { + LOCATE_FRAME_HEADER, + LOCATE_ID_FRAME1, + LOCATE_ID_FRAME2, + LOCATE_LENGTH_FRAME_H, + LOCATE_LENGTH_FRAME_L, + LOCATE_TYPE_FRAME1, + LOCATE_TYPE_FRAME2, + LOCATE_HEAD_CKSUM_FRAME, // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit] + LOCATE_DATA_FRAME, + LOCATE_DATA_CKSUM_FRAME, // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit] + LOCATE_PROCESS_FRAME, +}; + +class MR60BHA2Component : public Component, + public uart::UARTDevice { // The class name must be the name defined by text_sensor.py +#ifdef USE_SENSOR + SUB_SENSOR(breath_rate); + SUB_SENSOR(heart_rate); + SUB_SENSOR(distance); +#endif + + public: + float get_setup_priority() const override { return esphome::setup_priority::LATE; } + void dump_config() override; + void loop() override; + + protected: + bool validate_message_(); + void process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length); + + std::vector rx_message_; +}; + +} // namespace seeed_mr60bha2 +} // namespace esphome diff --git a/esphome/components/seeed_mr60bha2/sensor.py b/esphome/components/seeed_mr60bha2/sensor.py new file mode 100644 index 0000000000..5f30b363bf --- /dev/null +++ b/esphome/components/seeed_mr60bha2/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DISTANCE, + DEVICE_CLASS_DISTANCE, + ICON_HEART_PULSE, + ICON_PULSE, + ICON_SIGNAL, + STATE_CLASS_MEASUREMENT, + UNIT_BEATS_PER_MINUTE, + UNIT_CENTIMETER, +) + +from . import CONF_MR60BHA2_ID, MR60BHA2Component + +DEPENDENCIES = ["seeed_mr60bha2"] + +CONF_BREATH_RATE = "breath_rate" +CONF_HEART_RATE = "heart_rate" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MR60BHA2_ID): cv.use_id(MR60BHA2Component), + cv.Optional(CONF_BREATH_RATE): sensor.sensor_schema( + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_PULSE, + ), + cv.Optional(CONF_HEART_RATE): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_HEART_PULSE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_BEATS_PER_MINUTE, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CENTIMETER, + accuracy_decimals=2, + icon=ICON_SIGNAL, + ), + } +) + + +async def to_code(config): + mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID]) + if breath_rate_config := config.get(CONF_BREATH_RATE): + sens = await sensor.new_sensor(breath_rate_config) + cg.add(mr60bha2_component.set_breath_rate_sensor(sens)) + if heart_rate_config := config.get(CONF_HEART_RATE): + sens = await sensor.new_sensor(heart_rate_config) + cg.add(mr60bha2_component.set_heart_rate_sensor(sens)) + if distance_config := config.get(CONF_DISTANCE): + sens = await sensor.new_sensor(distance_config) + cg.add(mr60bha2_component.set_distance_sensor(sens)) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 67bd627f7f..a8a796853e 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -1,19 +1,19 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import i2c, sensor, sensirion_common from esphome import automation from esphome.automation import maybe_simple_id - +import esphome.codegen as cg +from esphome.components import i2c, sensirion_common, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_HUMIDITY, CONF_ID, CONF_OFFSET, CONF_PM_1_0, - CONF_PM_10_0, CONF_PM_2_5, CONF_PM_4_0, + CONF_PM_10_0, CONF_STORE_BASELINE, CONF_TEMPERATURE, + CONF_TEMPERATURE_COMPENSATION, DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PM1, @@ -51,7 +51,6 @@ CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" CONF_NOX = "nox" CONF_STD_INITIAL = "std_initial" -CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" CONF_TIME_CONSTANT = "time_constant" CONF_VOC = "voc" CONF_VOC_BASELINE = "voc_baseline" diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 13e859cc09..8c92f55ef7 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -1,23 +1,22 @@ import esphome.codegen as cg +from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv -from esphome.components import i2c, sensor, sensirion_common - from esphome.const import ( - CONF_COMPENSATION, - CONF_ID, CONF_BASELINE, + CONF_COMPENSATION, CONF_ECO2, + CONF_ID, CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, CONF_TVOC, - ICON_RADIATOR, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, - STATE_CLASS_MEASUREMENT, - UNIT_PARTS_PER_MILLION, - UNIT_PARTS_PER_BILLION, - ICON_MOLECULE_CO2, ENTITY_CATEGORY_DIAGNOSTIC, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, ) DEPENDENCIES = ["i2c"] @@ -77,7 +76,7 @@ CONFIG_SCHEMA = ( ), } ) - .extend(cv.polling_component_schema("1s")) + .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x58)) ) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 261604b992..77e9ef9820 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,8 +1,8 @@ #include "sgp30.h" +#include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" -#include namespace esphome { namespace sgp30 { @@ -295,10 +295,6 @@ void SGP30Component::update() { if (this->tvoc_sensor_ != nullptr) this->tvoc_sensor_->publish_state(tvoc); - if (this->get_update_interval() != 1000) { - ESP_LOGW(TAG, "Update interval for SGP30 sensor must be set to 1s for optimized readout"); - } - this->status_clear_warning(); this->send_env_data_(); this->read_iaq_baseline_(); diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 52afbf365e..3e6d680b89 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -97,11 +97,7 @@ RP_SPI_PINSETS = [ def get_target_platform(): - return ( - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] - if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE] - else "" - ) + return CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] def get_target_variant(): 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/spi_led_strip/light.py b/esphome/components/spi_led_strip/light.py index 78642935de..ca320265a9 100644 --- a/esphome/components/spi_led_strip/light.py +++ b/esphome/components/spi_led_strip/light.py @@ -1,8 +1,7 @@ import esphome.codegen as cg +from esphome.components import light, spi import esphome.config_validation as cv -from esphome.components import light -from esphome.components import spi -from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS +from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip") SpiLedStrip = spi_led_strip_ns.class_( @@ -18,8 +17,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS]) await light.register_light(var, config) await spi.register_spi_device(var, config) await cg.register_component(var, config) diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp new file mode 100644 index 0000000000..46243c0686 --- /dev/null +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -0,0 +1,67 @@ +#include "spi_led_strip.h" + +namespace esphome { +namespace spi_led_strip { + +SpiLedStrip::SpiLedStrip(uint16_t num_leds) { + this->num_leds_ = num_leds; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_size_ = num_leds * 4 + 8; + this->buf_ = allocator.allocate(this->buffer_size_); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); + return; + } + + this->effect_data_ = allocator.allocate(num_leds); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate effect data of size %u", num_leds); + return; + } + memset(this->buf_, 0xFF, this->buffer_size_); + memset(this->buf_, 0, 4); +} +void SpiLedStrip::setup() { + if (this->effect_data_ == nullptr || this->buf_ == nullptr) { + this->mark_failed(); + return; + } + this->spi_setup(); +} +light::LightTraits SpiLedStrip::get_traits() { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; +} +void SpiLedStrip::dump_config() { + esph_log_config(TAG, "SPI LED Strip:"); + esph_log_config(TAG, " LEDs: %d", this->num_leds_); + if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { + esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); + } else { + esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } +} +void SpiLedStrip::write_state(light::LightState *state) { + if (this->is_failed()) + return; + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + char strbuf[49]; + size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); + memset(strbuf, 0, sizeof(strbuf)); + for (size_t i = 0; i != len; i++) { + sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); + } + esph_log_v(TAG, "write_state: buf = %s", strbuf); + } + this->enable(); + this->write_array(this->buf_, this->buffer_size_); + this->disable(); +} +light::ESPColorView SpiLedStrip::get_view_internal(int32_t index) const { + size_t pos = index * 4 + 5; + return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, + this->effect_data_ + index, &this->correction_}; +} +} // namespace spi_led_strip +} // namespace esphome diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 1b317cdd69..14c5627ac3 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -13,74 +13,22 @@ class SpiLedStrip : public light::AddressableLight, public spi::SPIDevice { public: - void setup() override { this->spi_setup(); } + SpiLedStrip(uint16_t num_leds); + void setup() override; + float get_setup_priority() const override { return setup_priority::IO; } int32_t size() const override { return this->num_leds_; } - light::LightTraits get_traits() override { - auto traits = light::LightTraits(); - traits.set_supported_color_modes({light::ColorMode::RGB}); - return traits; - } - void set_num_leds(uint16_t num_leds) { - this->num_leds_ = num_leds; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->buffer_size_ = num_leds * 4 + 8; - this->buf_ = allocator.allocate(this->buffer_size_); - if (this->buf_ == nullptr) { - esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); - this->mark_failed(); - return; - } + light::LightTraits get_traits() override; - this->effect_data_ = allocator.allocate(num_leds); - if (this->effect_data_ == nullptr) { - esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds); - this->mark_failed(); - return; - } - memset(this->buf_, 0xFF, this->buffer_size_); - memset(this->buf_, 0, 4); - } + void dump_config() override; - void dump_config() override { - esph_log_config(TAG, "SPI LED Strip:"); - esph_log_config(TAG, " LEDs: %d", this->num_leds_); - if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { - esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); - } else { - esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); - } - } + void write_state(light::LightState *state) override; - void write_state(light::LightState *state) override { - if (this->is_failed()) - return; - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - char strbuf[49]; - size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); - memset(strbuf, 0, sizeof(strbuf)); - for (size_t i = 0; i != len; i++) { - sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); - } - esph_log_v(TAG, "write_state: buf = %s", strbuf); - } - this->enable(); - this->write_array(this->buf_, this->buffer_size_); - this->disable(); - } - - void clear_effect_data() override { - for (int i = 0; i < this->size(); i++) - this->effect_data_[i] = 0; - } + void clear_effect_data() override { memset(this->effect_data_, 0, this->num_leds_ * sizeof(this->effect_data_[0])); } protected: - light::ESPColorView get_view_internal(int32_t index) const override { - size_t pos = index * 4 + 5; - return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, - this->effect_data_ + index, &this->correction_}; - } + light::ESPColorView get_view_internal(int32_t index) const override; size_t buffer_size_{}; uint8_t *effect_data_{nullptr}; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 5384d29871..3cfb5ccdee 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -184,11 +184,13 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) { void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (valve != nullptr) { + if (this->state_ != IDLE) { // Only kill if not already idle + this->kill_(); // ensure everything is off before we let go! + } this->state_ = IDLE; // reset state this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it this->start_millis_ = 0; // reset because (new) valve has not been started yet this->stop_millis_ = 0; // reset because (new) valve has not been started yet - this->kill_(); // ensure everything is off before we let go! this->valve_ = valve; // finally, set the pointer to the new valve } } diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 33d36d6a69..ff4241a81f 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -106,8 +106,9 @@ void ToshibaClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 66931767b2..815a089d9f 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -120,8 +120,9 @@ light::LightTraits TuyaLight::get_traits() { traits.set_supported_color_modes( {light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); } - } else + } else { traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); } else if (this->color_id_.has_value()) { @@ -131,8 +132,9 @@ light::LightTraits TuyaLight::get_traits() { } else { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); } - } else + } else { traits.set_supported_color_modes({light::ColorMode::RGB}); + } } else if (this->dimmer_id_.has_value()) { traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); } else { diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 793c1d52f4..b241c03de4 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include "uart_component_esp32_arduino.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "uart_component_esp32_arduino.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -118,7 +118,7 @@ void ESP32ArduinoUARTComponent::setup() { } #endif // USE_LOGGER - if (next_uart_num >= UART_NUM_MAX) { + if (next_uart_num >= SOC_UART_NUM) { ESP_LOGW(TAG, "Maximum number of UART components created already."); this->mark_failed(); return; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6999dfb619..122d4105c8 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -1,11 +1,11 @@ #ifdef USE_ESP_IDF #include "uart_component_esp_idf.h" +#include #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -84,7 +84,7 @@ void IDFUARTComponent::setup() { } #endif // USE_LOGGER - if (next_uart_num >= UART_NUM_MAX) { + if (next_uart_num >= SOC_UART_NUM) { ESP_LOGW(TAG, "Maximum number of UART components created already."); this->mark_failed(); return; diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index ca15be2a80..e189975ade 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -85,7 +85,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(UDPComponent), cv.Optional(CONF_PORT, default=18511): cv.port, cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4 + cv.ipv4address, ), cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index b8727ec423..e29620fa9a 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -245,13 +245,9 @@ void UDPComponent::setup() { } struct sockaddr_in server {}; - socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); - if (sl == 0) { - ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); - this->status_set_error("Unable to set sockaddr"); - return; - } + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 9602d0c2d0..944fdfdee9 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -1,11 +1,12 @@ -import esphome.codegen as cg from esphome import automation -import esphome.config_validation as cv +import esphome.codegen as cg from esphome.components import i2c, sensor +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_EC, + CONF_ID, CONF_TEMPERATURE, + CONF_TEMPERATURE_COMPENSATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_EMPTY, @@ -18,7 +19,6 @@ DEPENDENCIES = ["i2c"] CONF_SOLUTION = "solution" CONF_TEMPERATURE_SENSOR = "temperature_sensor" -CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" ufire_ec_ns = cg.esphome_ns.namespace("ufire_ec") 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/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index c982cd69ab..b97426011b 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -38,6 +38,7 @@ def check_timeout(value): raise cv.Invalid("Maximum timeout can not be greater then 60 seconds") return value + def check_timing_budget(value): value = cv.positive_time_period_microseconds(value) if value.total_microseconds < 17000 or value.total_microseconds > 4294967295: diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index cb3b19aa1a..fb9e8ff6e5 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->command(0x01); // 1-0=11: internal power - this->data(0x07); - this->data(0x17); // VGH&VGL - this->data(0x3F); // VSH - this->data(0x26); // VSL - this->data(0x11); // VSHR + this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1 + this->data(0x17); // VGH&VGL ??? VCOM_SLEW=1 but this is fixed, VG_LVL[2:0]=111 => VGH=20V VGL=-20V, it could be 0x07 + this->data(0x3F); // VSH=15V? + this->data(0x26); // VSL=-9.4V? + this->data(0x11); // VSHR=5.8V? // VCOM DC Setting this->command(0x82); - this->data(0x24); // VCOM - - // Booster Setting - this->command(0x06); - this->data(0x27); - this->data(0x27); - this->data(0x2F); - this->data(0x17); + this->data(0x24); // VCOM=-1.9V // POWER ON this->command(0x04); - delay(100); // NOLINT this->wait_until_idle_(); + // COMMAND PANEL SETTING this->command(0x00); this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f @@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->data(0x20); this->data(0x01); // gate 480 this->data(0xE0); - // COMMAND ...? - this->command(0x15); - this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x20); this->data(0x00); + // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + // Resolution setting this->command(0x65); this->data(0x00); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0467023039..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); } @@ -1415,6 +1413,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(200, "application/json", data.c_str()); return; } + + auto call = obj->make_call(); + if (request->hasParam("code")) { + call.set_code(request->getParam("code")->value().c_str()); + } + + if (match.method == "disarm") { + call.disarm(); + } else if (match.method == "arm_away") { + call.arm_away(); + } else if (match.method == "arm_home") { + call.arm_home(); + } else if (match.method == "arm_night") { + call.arm_night(); + } else if (match.method == "arm_vacation") { + call.arm_vacation(); + } else { + request->send(404); + return; + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; } request->send(404); } @@ -1664,7 +1686,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #endif #ifdef USE_ALARM_CONTROL_PANEL - if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") return true; #endif diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ad1a4f5262..582b826de0 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -93,16 +93,16 @@ def validate_channel(value): AP_MANUAL_IP_SCHEMA = cv.Schema( { - cv.Required(CONF_STATIC_IP): cv.ipv4, - cv.Required(CONF_GATEWAY): cv.ipv4, - cv.Required(CONF_SUBNET): cv.ipv4, + cv.Required(CONF_STATIC_IP): cv.ipv4address, + cv.Required(CONF_GATEWAY): cv.ipv4address, + cv.Required(CONF_SUBNET): cv.ipv4address, } ) STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend( { - cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, - cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address, } ) @@ -364,7 +364,7 @@ def eap_auth(config): def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) - return IPAddress(*ip.args) + return IPAddress(str(ip)) def manual_ip(config): diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index bc10bbd1e5..b7a77fcdc9 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -11,10 +11,19 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif + +#ifdef USE_WIFI_AP +#include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + #include "lwip/apps/sntp.h" #include "lwip/dns.h" #include "lwip/err.h" +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING +#include "lwip/priv/tcpip_priv.h" +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -286,11 +295,26 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } if (!manual_ip.has_value()) { +// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!) +// https://github.com/esphome/issues/issues/6591 +// https://github.com/espressif/arduino-esp32/issues/10526 +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + LOCK_TCPIP_CORE(); + } +#endif + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, // the built-in SNTP client has a memory leak in certain situations. Disable this feature. // https://github.com/esphome/issues/issues/2299 sntp_servermode_dhcp(false); +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + UNLOCK_TCPIP_CORE(); + } +#endif + // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); @@ -638,7 +662,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - auto status = WiFiClass::status(); +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) + const auto status = WiFiClass::status(); +#else + const auto status = WiFi.status(); +#endif + if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; } diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 5e34a8a19b..fc0e4e0538 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -67,8 +67,8 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Wireguard), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_ADDRESS): cv.ipv4, - cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4, + cv.Required(CONF_ADDRESS): cv.ipv4address, + cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address, cv.Required(CONF_PRIVATE_KEY): _wireguard_key, cv.Required(CONF_PEER_ENDPOINT): cv.string, cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index 493c689b42..a3cf53ff66 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -104,8 +104,9 @@ void YashimaClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/config.py b/esphome/config.py index 7d48569d2d..65e9ac29bc 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ESPHOME, CONF_EXTERNAL_COMPONENTS, CONF_ID, + CONF_MIN_VERSION, CONF_PACKAGES, CONF_PLATFORM, CONF_SUBSTITUTIONS, @@ -839,6 +840,10 @@ def validate_config( # Remove temporary esphome config path again, it will be reloaded later result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME) + # Check version number now to avoid loading components that are not supported + if min_version := config[CONF_ESPHOME].get(CONF_MIN_VERSION): + cv.All(cv.version_number, cv.validate_esphome_version)(min_version) + # First run platform validation steps for key in TARGET_PLATFORMS: if key in config: diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ebfb2631c3..20a0774ccb 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime +from ipaddress import AddressValueError, IPv4Address, ip_address import logging import os import re @@ -67,7 +68,6 @@ from esphome.const import ( from esphome.core import ( CORE, HexInt, - IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, @@ -1130,7 +1130,7 @@ def domain(value): if re.match(vol.DOMAIN_REGEX, value) is not None: return value try: - return str(ipv4(value)) + return str(ipaddress(value)) except Invalid as err: raise Invalid(f"Invalid domain: {value}") from err @@ -1160,21 +1160,20 @@ def ssid(value): return value -def ipv4(value): - if isinstance(value, list): - parts = value - elif isinstance(value, str): - parts = value.split(".") - elif isinstance(value, IPAddress): - return value - else: - raise Invalid("IPv4 address must consist of either string or integer list") - if len(parts) != 4: - raise Invalid("IPv4 address must consist of four point-separated integers") - parts_ = list(map(int, parts)) - if not all(0 <= x < 256 for x in parts_): - raise Invalid("IPv4 address parts must be in range from 0 to 255") - return IPAddress(*parts_) +def ipv4address(value): + try: + address = IPv4Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 address") from exc + return address + + +def ipaddress(value): + try: + address = ip_address(value) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP address") from exc + return address def _valid_topic(value): @@ -1660,6 +1659,12 @@ class SplitDefault(Optional): esp32_c3=vol.UNDEFINED, esp32_c3_arduino=vol.UNDEFINED, esp32_c3_idf=vol.UNDEFINED, + esp32_c6=vol.UNDEFINED, + esp32_c6_arduino=vol.UNDEFINED, + esp32_c6_idf=vol.UNDEFINED, + esp32_h2=vol.UNDEFINED, + esp32_h2_arduino=vol.UNDEFINED, + esp32_h2_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1691,6 +1696,18 @@ class SplitDefault(Optional): self._esp32_c3_idf_default = vol.default_factory( _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) ) + self._esp32_c6_arduino_default = vol.default_factory( + _get_priority_default(esp32_c6_arduino, esp32_c6, esp32_arduino, esp32) + ) + self._esp32_c6_idf_default = vol.default_factory( + _get_priority_default(esp32_c6_idf, esp32_c6, esp32_idf, esp32) + ) + self._esp32_h2_arduino_default = vol.default_factory( + _get_priority_default(esp32_h2_arduino, esp32_h2, esp32_arduino, esp32) + ) + self._esp32_h2_idf_default = vol.default_factory( + _get_priority_default(esp32_h2_idf, esp32_h2, esp32_idf, esp32) + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1704,6 +1721,8 @@ class SplitDefault(Optional): from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32S2, VARIANT_ESP32S3, ) @@ -1724,6 +1743,16 @@ class SplitDefault(Optional): return self._esp32_c3_arduino_default if CORE.using_esp_idf: return self._esp32_c3_idf_default + elif variant == VARIANT_ESP32C6: + if CORE.using_arduino: + return self._esp32_c6_arduino_default + if CORE.using_esp_idf: + return self._esp32_c6_idf_default + elif variant == VARIANT_ESP32H2: + if CORE.using_arduino: + return self._esp32_h2_arduino_default + if CORE.using_esp_idf: + return self._esp32_h2_idf_default else: if CORE.using_arduino: return self._esp32_arduino_default diff --git a/esphome/const.py b/esphome/const.py index 3d3bfcc244..284f8d5f78 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.12.0-dev" +__version__ = "2025.2.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -131,7 +131,9 @@ CONF_CLIENT_CERTIFICATE = "client_certificate" CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" CONF_CLIENT_ID = "client_id" CONF_CLK_PIN = "clk_pin" +CONF_CLOCK_DIVIDER = "clock_divider" CONF_CLOCK_PIN = "clock_pin" +CONF_CLOCK_RESOLUTION = "clock_resolution" CONF_CLOSE_ACTION = "close_action" CONF_CLOSE_DURATION = "close_duration" CONF_CLOSE_ENDSTOP = "close_endstop" @@ -488,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" @@ -739,6 +742,7 @@ CONF_RGB_ORDER = "rgb_order" CONF_RGBW = "rgbw" CONF_RISING_EDGE = "rising_edge" CONF_RMT_CHANNEL = "rmt_channel" +CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" CONF_RS_PIN = "rs_pin" @@ -864,6 +868,7 @@ CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topi CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC = "target_temperature_low_state_topic" CONF_TARGET_TEMPERATURE_STATE_TOPIC = "target_temperature_state_topic" CONF_TEMPERATURE = "temperature" +CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" @@ -917,6 +922,7 @@ CONF_UPDATE_ON_BOOT = "update_on_boot" CONF_URL = "url" CONF_USE_ABBREVIATIONS = "use_abbreviations" CONF_USE_ADDRESS = "use_address" +CONF_USE_DMA = "use_dma" CONF_USE_FAHRENHEIT = "use_fahrenheit" CONF_USERNAME = "username" CONF_UUID = "uuid" @@ -1001,6 +1007,7 @@ ICON_GRAIN = "mdi:grain" ICON_GYROSCOPE_X = "mdi:axis-x-rotate-clockwise" ICON_GYROSCOPE_Y = "mdi:axis-y-rotate-clockwise" ICON_GYROSCOPE_Z = "mdi:axis-z-rotate-clockwise" +ICON_HEART_PULSE = "mdi:heart-pulse" ICON_HEATING_COIL = "mdi:heating-coil" ICON_KEY_PLUS = "mdi:key-plus" ICON_LIGHTBULB = "mdi:lightbulb" @@ -1040,6 +1047,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy" ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" +UNIT_BEATS_PER_MINUTE = "bpm" UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³" UNIT_BYTES = "B" UNIT_CELSIUS = "°C" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a97c3b18c9..f26c3da483 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -54,16 +54,6 @@ class HexInt(int): return f"{sign}0x{value:X}" -class IPAddress: - def __init__(self, *args): - if len(args) != 4: - raise ValueError("IPAddress must consist of 4 items") - self.args = args - - def __str__(self): - return ".".join(str(x) for x in self.args) - - class MACAddress: def __init__(self, *parts): if len(parts) != 6: diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index dcf7da2f21..13179b90bb 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -2,6 +2,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index eb3b20d007..c38a26c6a8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -49,6 +49,7 @@ #define USE_LVGL_IMAGE #define USE_LVGL_KEY_LISTENER #define USE_LVGL_KEYBOARD +#define USE_LVGL_METER #define USE_LVGL_ROLLER #define USE_LVGL_ROTARY_ENCODER #define USE_LVGL_TOUCHSCREEN diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 4e8caeae99..439bb2ccb0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -45,7 +45,9 @@ #endif #ifdef USE_ESP32 #include "esp32/rom/crc.h" - +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2) +#include "esp_mac.h" +#endif #include "esp_efuse.h" #include "esp_efuse_table.h" #endif @@ -126,19 +128,21 @@ uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse } } else #endif - if (reverse_poly == 0xa001) { - while (len--) { - uint8_t combo = crc ^ (uint8_t) *data++; - crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; - } - } else { - while (len--) { - crc ^= *data++; - for (uint8_t i = 0; i < 8; i++) { - if (crc & 0x0001) { - crc = (crc >> 1) ^ reverse_poly; - } else { - crc >>= 1; + { + if (reverse_poly == 0xa001) { + while (len--) { + uint8_t combo = crc ^ (uint8_t) *data++; + crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; + } + } else { + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x0001) { + crc = (crc >> 1) ^ reverse_poly; + } else { + crc >>= 1; + } } } } @@ -259,7 +263,7 @@ bool random_bytes(uint8_t *data, size_t len) { bool str_equals_case_insensitive(const std::string &a, const std::string &b) { return strcasecmp(a.c_str(), b.c_str()) == 0; } -#if ESP_IDF_VERSION_MAJOR >= 5 +#if __cplusplus >= 202002L bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); } bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); } #else @@ -767,7 +771,8 @@ bool mac_address_is_valid(const uint8_t *mac) { return !(is_all_zeros || is_all_ones); } -void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability +void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) { + // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index fcbd8d8683..82b0fe07f8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -11,6 +11,14 @@ #include "esphome/core/optional.h" +#ifdef USE_ESP8266 +#include +#endif + +#ifdef USE_RP2040 +#include +#endif + #ifdef USE_ESP32 #include #endif @@ -155,7 +163,7 @@ template T remap(U value, U min, U max, T min_out, T max return (value - min) * (max_out - min_out) / (max - min) + min_out; } -/// Calculate a CRC-8 checksum of \p data with size \p len. +/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial. uint8_t crc8(const uint8_t *data, uint8_t len); /// Calculate a CRC-16 checksum of \p data with size \p len. @@ -684,20 +692,23 @@ template class RAMAllocator { }; RAMAllocator() = default; - RAMAllocator(uint8_t flags) : flags_{flags} {} + RAMAllocator(uint8_t flags) { + // default is both external and internal + flags &= ALLOC_INTERNAL | ALLOC_EXTERNAL; + if (flags != 0) + this->flags_ = flags; + } template constexpr RAMAllocator(const RAMAllocator &other) : flags_{other.flags_} {} T *allocate(size_t n) { size_t size = n * sizeof(T); T *ptr = nullptr; #ifdef USE_ESP32 - // External allocation by default or if explicitely requested - if ((this->flags_ & Flags::ALLOC_EXTERNAL) || ((this->flags_ & Flags::ALLOC_INTERNAL) == 0)) { + if (this->flags_ & Flags::ALLOC_EXTERNAL) { ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); } - // Fallback to internal allocation if explicitely requested or no flag is specified - if (ptr == nullptr && ((this->flags_ & Flags::ALLOC_INTERNAL) || (this->flags_ & Flags::ALLOC_EXTERNAL) == 0)) { - ptr = static_cast(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) { + ptr = static_cast(heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); } #else // Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported @@ -710,8 +721,46 @@ template class RAMAllocator { free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) } + /** + * Return the total heap space available via this allocator + */ + size_t get_free_heap_size() const { +#ifdef USE_ESP8266 + return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + auto max_internal = + this->flags_ & ALLOC_INTERNAL ? heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL) : 0; + auto max_external = + this->flags_ & ALLOC_EXTERNAL ? heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM) : 0; + return max_internal + max_external; +#elif defined(USE_RP2040) + return ::rp2040.getFreeHeap(); +#elif defined(USE_LIBRETINY) + return lt_heap_get_free(); +#else + return 100000; +#endif + } + + /** + * Return the maximum size block this allocator could allocate. This may be an approximation on some platforms + */ + size_t get_max_free_block_size() const { +#ifdef USE_ESP8266 + return ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) +#elif defined(USE_ESP32) + auto max_internal = + this->flags_ & ALLOC_INTERNAL ? heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL) : 0; + auto max_external = + this->flags_ & ALLOC_EXTERNAL ? heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM) : 0; + return std::max(max_internal, max_external); +#else + return this->get_free_heap_size(); +#endif + } + private: - uint8_t flags_{Flags::ALLOW_FAILURE}; + uint8_t flags_{ALLOC_INTERNAL | ALLOC_EXTERNAL}; }; template using ExternalRAMAllocator = RAMAllocator; diff --git a/esphome/core/log.h b/esphome/core/log.h index 86af534f98..99a68024c5 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -74,7 +74,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #define esph_log_vv(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERY_VERBOSE #else @@ -83,7 +83,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #define esph_log_v(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERBOSE #else @@ -92,9 +92,9 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #define esph_log_d(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define esph_log_config(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_DEBUG #define ESPHOME_LOG_HAS_CONFIG @@ -105,7 +105,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO #define esph_log_i(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_INFO #else @@ -114,7 +114,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN #define esph_log_w(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_WARN #else @@ -123,7 +123,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR #define esph_log_e(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_ERROR #else diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index 6152ada314..f779531263 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -13,8 +13,8 @@ static const char *const TAG = "ring_buffer"; RingBuffer::~RingBuffer() { if (this->handle_ != nullptr) { - vStreamBufferDelete(this->handle_); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + vRingbufferDelete(this->handle_); + RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE); allocator.deallocate(this->storage_, this->size_); } } @@ -22,26 +22,49 @@ RingBuffer::~RingBuffer() { std::unique_ptr RingBuffer::create(size_t len) { std::unique_ptr rb = make_unique(); - rb->size_ = len + 1; + rb->size_ = len; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE); rb->storage_ = allocator.allocate(rb->size_); if (rb->storage_ == nullptr) { return nullptr; } - rb->handle_ = xStreamBufferCreateStatic(rb->size_, 1, rb->storage_, &rb->structure_); + rb->handle_ = xRingbufferCreateStatic(rb->size_, RINGBUF_TYPE_BYTEBUF, rb->storage_, &rb->structure_); ESP_LOGD(TAG, "Created ring buffer with size %u", len); + return rb; } size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { - if (ticks_to_wait > 0) - xStreamBufferSetTriggerLevel(this->handle_, len); + size_t bytes_read = 0; - size_t bytes_read = xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); + void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, ticks_to_wait, len); - xStreamBufferSetTriggerLevel(this->handle_, 1); + if (buffer_data == nullptr) { + return 0; + } + + std::memcpy(data, buffer_data, bytes_read); + + vRingbufferReturnItem(this->handle_, buffer_data); + + if (bytes_read < len) { + // Data may have wrapped around, so read a second time to receive the remainder + size_t follow_up_bytes_read = 0; + size_t bytes_remaining = len - bytes_read; + + buffer_data = xRingbufferReceiveUpTo(this->handle_, &follow_up_bytes_read, 0, bytes_remaining); + + if (buffer_data == nullptr) { + return bytes_read; + } + + std::memcpy((void *) ((uint8_t *) (data) + bytes_read), buffer_data, follow_up_bytes_read); + + vRingbufferReturnItem(this->handle_, buffer_data); + bytes_read += follow_up_bytes_read; + } return bytes_read; } @@ -49,22 +72,55 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { size_t RingBuffer::write(const void *data, size_t len) { size_t free = this->free(); if (free < len) { - size_t needed = len - free; - uint8_t discard[needed]; - xStreamBufferReceive(this->handle_, discard, needed, 0); + // Free enough space in the ring buffer to fit the new data + this->discard_bytes_(len - free); } - return xStreamBufferSend(this->handle_, data, len, 0); + return this->write_without_replacement(data, len, 0); } size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) { - return xStreamBufferSend(this->handle_, data, len, ticks_to_wait); + if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) { + // Couldn't fit all the data, so only write what will fit + size_t free = std::min(this->free(), len); + if (xRingbufferSend(this->handle_, data, free, 0)) { + return free; + } + return 0; + } + return len; } -size_t RingBuffer::available() const { return xStreamBufferBytesAvailable(this->handle_); } +size_t RingBuffer::available() const { + UBaseType_t ux_items_waiting = 0; + vRingbufferGetInfo(this->handle_, nullptr, nullptr, nullptr, nullptr, &ux_items_waiting); + return ux_items_waiting; +} -size_t RingBuffer::free() const { return xStreamBufferSpacesAvailable(this->handle_); } +size_t RingBuffer::free() const { return xRingbufferGetCurFreeSize(this->handle_); } -BaseType_t RingBuffer::reset() { return xStreamBufferReset(this->handle_); } +BaseType_t RingBuffer::reset() { + // Discards all the available data + return this->discard_bytes_(this->available()); +} + +bool RingBuffer::discard_bytes_(size_t discard_bytes) { + size_t bytes_read = 0; + + void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, 0, discard_bytes); + if (buffer_data != nullptr) + vRingbufferReturnItem(this->handle_, buffer_data); + + if (bytes_read < discard_bytes) { + size_t wrapped_bytes_read = 0; + buffer_data = xRingbufferReceiveUpTo(this->handle_, &wrapped_bytes_read, 0, discard_bytes - bytes_read); + if (buffer_data != nullptr) { + vRingbufferReturnItem(this->handle_, buffer_data); + bytes_read += wrapped_bytes_read; + } + } + + return (bytes_read == discard_bytes); +} } // namespace esphome diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index aade1b5f49..bad96d3181 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -3,7 +3,7 @@ #ifdef USE_ESP32 #include -#include +#include #include #include @@ -82,9 +82,14 @@ class RingBuffer { static std::unique_ptr create(size_t len); protected: - StreamBufferHandle_t handle_; - StaticStreamBuffer_t structure_; - uint8_t *storage_; + /// @brief Discards data from the ring buffer. + /// @param discard_bytes amount of bytes to discard + /// @return True if all bytes were successfully discarded, false otherwise + bool discard_bytes_(size_t discard_bytes); + + RingbufHandle_t handle_{nullptr}; + StaticRingbuffer_t structure_; + uint8_t *storage_{nullptr}; size_t size_{0}; }; diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 0fed8e9c53..67712da9b6 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -108,6 +108,12 @@ def is_authenticated(handler: BaseHandler) -> bool: return True if settings.using_auth: + if auth_header := handler.request.headers.get("Authorization"): + assert isinstance(auth_header, str) + if auth_header.startswith("Basic "): + auth_decoded = base64.b64decode(auth_header[6:]).decode() + username, password = auth_decoded.split(":", 1) + return settings.check_password(username, password) return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES return True diff --git a/esphome/log.py b/esphome/log.py index 23dc453d32..835cd6b44d 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -67,20 +67,18 @@ class ESPHomeLogFormatter(logging.Formatter): def setup_log( - debug: bool = False, quiet: bool = False, include_timestamp: bool = False + log_level=logging.INFO, + include_timestamp: bool = False, ) -> None: import colorama colorama.init() - if debug: - log_level = logging.DEBUG + if log_level == logging.DEBUG: CORE.verbose = True - elif quiet: - log_level = logging.CRITICAL + elif log_level == logging.CRITICAL: CORE.quiet = True - else: - log_level = logging.INFO + logging.basicConfig(level=log_level) logging.getLogger("urllib3").setLevel(logging.WARNING) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d67511dfec..b27ce4c3e3 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -4,6 +4,7 @@ import fnmatch import functools import inspect from io import TextIOWrapper +from ipaddress import _BaseAddress import logging import math import os @@ -25,7 +26,6 @@ from esphome.core import ( CORE, DocumentRange, EsphomeError, - IPAddress, Lambda, MACAddress, TimePeriod, @@ -576,7 +576,7 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool) ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) -ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) diff --git a/requirements.txt b/requirements.txt index 7bc1c895df..d96004c8ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.16 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 -esphome-dashboard==20241120.0 +esphome-dashboard==20241217.1 aioesphomeapi==24.6.2 zeroconf==0.132.2 puremagic==1.27 diff --git a/script/build_codeowners.py b/script/build_codeowners.py index db34ad7702..523fe8ac7f 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -5,7 +5,7 @@ from pathlib import Path import sys from esphome.config import get_component, get_platform -from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM from esphome.core import CORE from esphome.helpers import write_file_if_changed @@ -39,7 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) -CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None} codeowners = defaultdict(list) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 2023dc0402..07093e179a 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -85,12 +85,12 @@ def load_components(): # pylint: disable=wrong-import-position -from esphome.const import CONF_TYPE, KEY_CORE +from esphome.const import CONF_TYPE, KEY_CORE, KEY_TARGET_PLATFORM from esphome.core import CORE # pylint: enable=wrong-import-position -CORE.data[KEY_CORE] = {} +CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: None} load_components() # Import esphome after loading components (so schema is tracked) diff --git a/script/ci-custom.py b/script/ci-custom.py index 81e3da311a..d5d3ab88c8 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -58,7 +58,19 @@ file_types = ( ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") py_include = ("*.py",) -ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") +ignore_types = ( + ".ico", + ".png", + ".woff", + ".woff2", + "", + ".ttf", + ".otf", + ".pcf", + ".apng", + ".gif", + ".webp", +) LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] @@ -669,8 +681,7 @@ def main(): ) args = parser.parse_args() - global EXECUTABLE_BIT - EXECUTABLE_BIT = git_ls_files() + EXECUTABLE_BIT.update(git_ls_files()) files = list(EXECUTABLE_BIT.keys()) # Match against re file_name_re = re.compile("|".join(args.files)) 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/adc/test.bk72xx-ard.yaml b/tests/components/adc/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..491d0af3b9 --- /dev/null +++ b/tests/components/adc/test.bk72xx-ard.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: P23 + name: Basic ADC Test diff --git a/tests/components/addressable_light/test.esp32-c3-idf.yaml b/tests/components/addressable_light/test.esp32-c3-idf.yaml index f587113fac..7b3516345d 100644 --- a/tests/components/addressable_light/test.esp32-c3-idf.yaml +++ b/tests/components/addressable_light/test.esp32-c3-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 display: - platform: addressable_light diff --git a/tests/components/addressable_light/test.esp32-idf.yaml b/tests/components/addressable_light/test.esp32-idf.yaml index f587113fac..7b3516345d 100644 --- a/tests/components/addressable_light/test.esp32-idf.yaml +++ b/tests/components/addressable_light/test.esp32-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 display: - platform: addressable_light diff --git a/tests/components/animation/.gitattributes b/tests/components/animation/.gitattributes new file mode 100644 index 0000000000..ff9fc6f1f1 --- /dev/null +++ b/tests/components/animation/.gitattributes @@ -0,0 +1,4 @@ +*.apng -text +*.webp -text +*.gif -text + diff --git a/tests/components/animation/anim.apng b/tests/components/animation/anim.apng new file mode 100644 index 0000000000..927af5eb05 Binary files /dev/null and b/tests/components/animation/anim.apng differ diff --git a/tests/components/animation/anim.gif b/tests/components/animation/anim.gif new file mode 100644 index 0000000000..9932e77448 Binary files /dev/null and b/tests/components/animation/anim.gif differ diff --git a/tests/components/animation/anim.webp b/tests/components/animation/anim.webp new file mode 100644 index 0000000000..8958377afa Binary files /dev/null and b/tests/components/animation/anim.webp differ diff --git a/tests/components/animation/common.yaml b/tests/components/animation/common.yaml new file mode 100644 index 0000000000..c0f04fe768 --- /dev/null +++ b/tests/components/animation/common.yaml @@ -0,0 +1,23 @@ +animation: + - id: rgb565_animation + file: $component_dir/anim.gif + type: RGB565 + use_transparency: opaque + resize: 50x50 + - id: rgb_animation + file: $component_dir/anim.apng + type: RGB + use_transparency: chroma_key + resize: 50x50 + - id: grayscale_animation + file: $component_dir/anim.apng + type: grayscale + +display: + lambda: |- + id(rgb565_animation).next_frame(); + id(rgb_animation1).next_frame(); + id(grayscale_animation2).next_frame(); + it.image(0, 0, rgb565_animation); + it.image(120, 0, rgb_animation1); + it.image(240, 0, grayscale_animation2); diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml index af6cd202dd..5d330900df 100644 --- a/tests/components/animation/test.esp32-ard.yaml +++ b/tests/components/animation/test.esp32-ard.yaml @@ -13,12 +13,6 @@ display: reset_pin: 21 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: +packages: + animation: !include common.yaml -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false diff --git a/tests/components/animation/test.esp32-c3-ard.yaml b/tests/components/animation/test.esp32-c3-ard.yaml index 10e8ccb47e..18aa2a5b06 100644 --- a/tests/components/animation/test.esp32-c3-ard.yaml +++ b/tests/components/animation/test.esp32-c3-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 10 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml index 10e8ccb47e..18aa2a5b06 100644 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -13,12 +13,5 @@ display: reset_pin: 10 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml index af6cd202dd..7d9fe45bff 100644 --- a/tests/components/animation/test.esp32-idf.yaml +++ b/tests/components/animation/test.esp32-idf.yaml @@ -13,12 +13,5 @@ display: reset_pin: 21 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp8266-ard.yaml b/tests/components/animation/test.esp8266-ard.yaml index ced4996f25..9548c7fbeb 100644 --- a/tests/components/animation/test.esp8266-ard.yaml +++ b/tests/components/animation/test.esp8266-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 16 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 0e33959cc6..efb3f2907c 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 22 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-ard.yaml b/tests/components/debug/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-ard.yaml b/tests/components/debug/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/debug/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml index 25304cd3b4..a27e62c1fb 100644 --- a/tests/components/e131/test.esp32-c3-idf.yaml +++ b/tests/components/e131/test.esp32-c3-idf.yaml @@ -12,7 +12,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - e131: universe: 1 diff --git a/tests/components/e131/test.esp32-idf.yaml b/tests/components/e131/test.esp32-idf.yaml index 25304cd3b4..a27e62c1fb 100644 --- a/tests/components/e131/test.esp32-idf.yaml +++ b/tests/components/e131/test.esp32-idf.yaml @@ -12,7 +12,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - e131: universe: 1 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/esp32_rmt_led_strip/test.esp32-c3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml index 8d04d3370b..93318d0581 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml @@ -3,14 +3,12 @@ light: id: led_strip pin: 4 num_leds: 60 - rmt_channel: 0 rgb_order: GRB chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 pin: 5 num_leds: 60 - rmt_channel: 1 rgb_order: RGB bit0_high: 100µs bit0_low: 100µs diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml index 6e1763b339..228af17189 100644 --- a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml +++ b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml @@ -3,14 +3,12 @@ light: id: led_strip pin: 13 num_leds: 60 - rmt_channel: 6 rgb_order: GRB chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 pin: 14 num_leds: 60 - rmt_channel: 2 rgb_order: RGB bit0_high: 100µs bit0_low: 100µs 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/image/common.yaml b/tests/components/image/common.yaml index 313da6bc0b..fdb0493d2a 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -5,32 +5,65 @@ image: dither: FloydSteinberg - id: transparent_transparent_image file: ../../pnglogo.png - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key + - id: rgba_image file: ../../pnglogo.png - type: RGBA + type: RGB + use_transparency: alpha_channel resize: 50x50 - id: rgb24_image file: ../../pnglogo.png - type: RGB24 - use_transparency: yes + type: RGB + use_transparency: chroma_key + - id: rgb_image + file: ../../pnglogo.png + type: RGB + use_transparency: opaque + - id: rgb565_image file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: opaque + - id: rgb565_ck_image + file: ../../pnglogo.png + type: RGB565 + use_transparency: chroma_key + - id: rgb565_alpha_image + file: ../../pnglogo.png + type: RGB565 + use_transparency: alpha_channel + + - id: grayscale_alpha_image + file: ../../pnglogo.png + type: grayscale + use_transparency: alpha_channel + resize: 50x50 + - id: grayscale_ck_image + file: ../../pnglogo.png + type: grayscale + use_transparency: chroma_key + - id: grayscale_image + file: ../../pnglogo.png + type: grayscale + use_transparency: opaque + - id: web_svg_image file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg resize: 256x48 - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key - id: web_tiff_image file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff - type: RGB24 + type: RGB resize: 48x48 - id: web_redirect_image file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 - type: RGB24 + type: RGB resize: 48x48 - id: mdi_alert + type: BINARY file: mdi:alert-circle-outline resize: 50x50 - id: another_alert_icon diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 29509db66c..61ecd5e374 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -5,4 +5,44 @@ display: width: 480 height: 480 -<<: !include common.yaml +image: + binary: + - id: binary_image + file: ../../pnglogo.png + dither: FloydSteinberg + - id: transparent_transparent_image + file: ../../pnglogo.png + use_transparency: chroma_key + rgb: + alpha_channel: + - id: rgba_image + file: ../../pnglogo.png + resize: 50x50 + chroma_key: + - id: rgb24_image + file: ../../pnglogo.png + type: RGB + opaque: + - id: rgb_image + file: ../../pnglogo.png + rgb565: + - id: rgb565_image + file: ../../pnglogo.png + use_transparency: opaque + - id: rgb565_ck_image + file: ../../pnglogo.png + use_transparency: chroma_key + - id: rgb565_alpha_image + file: ../../pnglogo.png + use_transparency: alpha_channel + grayscale: + - id: grayscale_alpha_image + file: ../../pnglogo.png + use_transparency: alpha_channel + resize: 50x50 + - id: grayscale_ck_image + file: ../../pnglogo.png + use_transparency: chroma_key + - id: grayscale_image + file: ../../pnglogo.png + use_transparency: opaque diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 4b7e13db91..b3227bb96e 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -27,6 +27,7 @@ lvgl: bg_color: light_blue disp_bg_color: color_id disp_bg_image: cat_image + disp_bg_opa: cover theme: obj: border_width: 1 @@ -132,10 +133,16 @@ lvgl: pages: - id: page1 + bg_image_src: cat_image on_load: - logger.log: page loaded - lvgl.widget.focus: action: restore + - if: + condition: + lvgl.page.is_showing: page1 + then: + logger.log: "Yes, page1 showing" on_unload: - logger.log: page unloaded - lvgl.widget.focus: mark @@ -165,6 +172,11 @@ lvgl: - Nov - Dec selected_index: 1 + on_change: + then: + - logger.log: + format: "Roller changed = %d: %s" + args: [x, text.c_str()] on_value: then: - logger.log: @@ -201,7 +213,7 @@ lvgl: - lvgl.animimg.stop: anim_img - lvgl.update: disp_bg_color: 0xffff00 - disp_bg_image: cat_image + disp_bg_image: none - lvgl.widget.show: message_box - label: text: "Hello shiny day" @@ -451,6 +463,7 @@ lvgl: src: cat_image align: top_left y: "50" + mode: real - tileview: id: tileview_id scrollbar_mode: active @@ -647,6 +660,7 @@ lvgl: grid_cell_row_pos: 0 grid_cell_column_pos: 0 src: !lambda return dog_image; + mode: virtual on_click: then: - lvgl.tabview.select: @@ -781,6 +795,34 @@ lvgl: color: 0xA0A0A0 r_mod: -20 opa: 0% + - id: page3 + widgets: + - keyboard: + id: lv_keyboard + align: bottom_mid + on_value: + then: + - 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" @@ -791,10 +833,13 @@ image: - id: cat_image resize: 256x48 file: $component_dir/logo-text.svg + type: RGB565 + use_transparency: alpha_channel - id: dog_image file: $component_dir/logo-text.svg resize: 256x48 - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key color: - id: light_blue diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 34918cb113..39d9a0ebf3 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -7,7 +7,6 @@ display: height: 320 - platform: sdl id: sdl1 - auto_clear_enabled: false dimensions: width: 480 height: 480 @@ -40,4 +39,3 @@ lvgl: text: Click ME on_click: logger.log: Clicked - diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index e84cd08422..589afcfefb 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -1,5 +1,9 @@ esphome: on_boot: + - lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));' + + - display.nextion.set_brightness: 80% + # Binary sensor publish action tests - binary_sensor.nextion.publish: id: r0_sensor diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 5c6feb4c81..81f43e9fdc 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -13,33 +13,32 @@ online_image: resize: 50x50 - id: online_binary_transparent_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key format: png - id: online_rgba_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG - type: RGBA + type: RGB + use_transparency: alpha_channel - id: online_rgb24_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG - type: RGB24 - use_transparency: true + type: RGB + use_transparency: chroma_key # Check the set_url action -time: - - platform: sntp - on_time: - - at: "13:37:42" - then: - - online_image.set_url: - id: online_rgba_image - url: http://www.example.org/example.png - - online_image.set_url: - id: online_rgba_image - url: !lambda |- - return "http://www.example.org/example.png"; - - online_image.set_url: - id: online_rgba_image - url: !lambda |- - return str_sprintf("http://homeassistant.local:8123"); - +esphome: + on_boot: + then: + - online_image.set_url: + id: online_rgba_image + url: http://www.example.org/example.png + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return "http://www.example.org/example.png"; + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return str_sprintf("http://homeassistant.local:8123"); diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 744580f18b..5edacc6f17 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -16,6 +16,19 @@ opentherm: summer_mode_active: true dhw_block: true sync_mode: true + controller_product_type: 63 + controller_product_version: 1 + opentherm_version_controller: 2.2 + controller_id: 1 + controller_configuration: 1 + before_send: + then: + - lambda: |- + ESP_LOGW("OT", ">> Sending message %d", x.id); + before_process_response: + then: + - lambda: |- + ESP_LOGW("OT", "<< Processing response %d", x.id); output: - platform: opentherm diff --git a/tests/components/partition/test.esp32-c3-idf.yaml b/tests/components/partition/test.esp32-c3-idf.yaml index 77cfc5ad44..397e1b0642 100644 --- a/tests/components/partition/test.esp32-c3-idf.yaml +++ b/tests/components/partition/test.esp32-c3-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 - platform: partition name: Partition Light segments: diff --git a/tests/components/partition/test.esp32-idf.yaml b/tests/components/partition/test.esp32-idf.yaml index 77cfc5ad44..397e1b0642 100644 --- a/tests/components/partition/test.esp32-idf.yaml +++ b/tests/components/partition/test.esp32-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 - platform: partition name: Partition Light segments: 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/psram/test.esp32-s2-ard.yaml b/tests/components/psram/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s2-idf.yaml b/tests/components/psram/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-ard.yaml b/tests/components/psram/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml new file mode 100644 index 0000000000..23589aed22 --- /dev/null +++ b/tests/components/remote_receiver/common-actions.yaml @@ -0,0 +1,144 @@ +on_abbwelcome: + then: + - logger.log: + format: "on_abbwelcome: %u" + args: ["x.data()[0]"] +on_aeha: + then: + - logger.log: + format: "on_aeha: %u %u" + args: ["x.address", "x.data.front()"] +on_byronsx: + then: + - logger.log: + format: "on_byronsx: %u %u" + args: ["x.address", "x.command"] +on_canalsat: + then: + - logger.log: + format: "on_canalsat: %u %u" + args: ["x.address", "x.command"] +# on_canalsatld: +# then: +# - logger.log: +# format: "on_canalsatld: %u %u" +# args: ["x.address", "x.command"] +on_coolix: + then: + - logger.log: + format: "on_coolix: %lu %lu" + args: ["long(x.first)", "long(x.second)"] +on_dish: + then: + - logger.log: + format: "on_dish: %u %u" + args: ["x.address", "x.command"] +on_dooya: + then: + - logger.log: + format: "on_dooya: %u %u %u" + args: ["x.channel", "x.button", "x.check"] +on_drayton: + then: + - logger.log: + format: "on_drayton: %u %u %u" + args: ["x.address", "x.channel", "x.command"] +on_jvc: + then: + - logger.log: + format: "on_jvc: %lu" + args: ["long(x.data)"] +on_keeloq: + then: + - logger.log: + format: "on_keeloq: %lu %lu %u" + args: ["long(x.encrypted)", "long(x.address)", "x.command"] +on_haier: + then: + - logger.log: + format: "on_haier: %u" + args: ["x.data.front()"] +on_lg: + then: + - logger.log: + format: "on_lg: %lu %u" + args: ["long(x.data)", "x.nbits"] +on_magiquest: + then: + - logger.log: + format: "on_magiquest: %u %lu" + args: ["x.magnitude", "long(x.wand_id)"] +on_midea: + then: + - logger.log: + format: "on_midea: %u %u" + args: ["x.size()", "x.data()[0]"] +on_nec: + then: + - logger.log: + format: "on_nec: %u %u" + args: ["x.address", "x.command"] +on_nexa: + then: + - logger.log: + format: "on_nexa: %lu %u %u %u %u" + args: ["long(x.device)", "x.group", "x.state", "x.channel", "x.level"] +on_panasonic: + then: + - logger.log: + format: "on_panasonic: %u %lu" + args: ["x.address", "long(x.command)"] +on_pioneer: + then: + - logger.log: + format: "on_pioneer: %u %u" + args: ["x.rc_code_1", "x.rc_code_2"] +on_pronto: + then: + - logger.log: + format: "on_pronto: %s" + args: ["x.data.c_str()"] +on_raw: + then: + - logger.log: + format: "on_raw: %lu" + args: ["long(x.front())"] +on_rc5: + then: + - logger.log: + format: "on_rc5: %u %u" + args: ["x.address", "x.command"] +on_rc6: + then: + - logger.log: + format: "on_rc6: %u %u" + args: ["x.address", "x.command"] +on_rc_switch: + then: + - logger.log: + format: "on_rc_switch: %llu %u" + args: ["x.code", "x.protocol"] +on_samsung: + then: + - logger.log: + format: "on_samsung: %llu %u" + args: ["x.data", "x.nbits"] +on_samsung36: + then: + - logger.log: + format: "on_samsung36: %u %lu" + args: ["x.address", "long(x.command)"] +on_sony: + then: + - logger.log: + format: "on_sony: %lu %u" + args: ["long(x.data)", "x.nbits"] +on_toshiba_ac: + then: + - logger.log: + format: "on_toshiba_ac: %llu %llu" + args: ["x.rc_code_1", "x.rc_code_2"] +on_mirage: + then: + - lambda: |- + ESP_LOGD("mirage", "Mirage data: %s", format_hex(x.data).c_str()); diff --git a/tests/components/remote_receiver/esp32-common-ard.yaml b/tests/components/remote_receiver/esp32-common-ard.yaml new file mode 100644 index 0000000000..e331a35307 --- /dev/null +++ b/tests/components/remote_receiver/esp32-common-ard.yaml @@ -0,0 +1,14 @@ +remote_receiver: + - id: rcvr + pin: ${pin} + rmt_channel: ${rmt_channel} + dump: all + tolerance: 25% + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_receiver/esp32-common-idf.yaml b/tests/components/remote_receiver/esp32-common-idf.yaml new file mode 100644 index 0000000000..b314880f8a --- /dev/null +++ b/tests/components/remote_receiver/esp32-common-idf.yaml @@ -0,0 +1,18 @@ +remote_receiver: + - id: rcvr + pin: ${pin} + dump: all + tolerance: 25% + clock_resolution: ${clock_resolution} + filter_symbols: ${filter_symbols} + receive_symbols: ${receive_symbols} + rmt_symbols: ${rmt_symbols} + use_dma: ${use_dma} + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_receiver/esp32-common.yaml b/tests/components/remote_receiver/esp32-common.yaml deleted file mode 100644 index 7e5d2cce32..0000000000 --- a/tests/components/remote_receiver/esp32-common.yaml +++ /dev/null @@ -1,157 +0,0 @@ -remote_receiver: - id: rcvr - pin: ${pin} - rmt_channel: ${rmt_channel} - dump: all - tolerance: 25% - on_abbwelcome: - then: - - logger.log: - format: "on_abbwelcome: %u" - args: ["x.data()[0]"] - on_aeha: - then: - - logger.log: - format: "on_aeha: %u %u" - args: ["x.address", "x.data.front()"] - on_byronsx: - then: - - logger.log: - format: "on_byronsx: %u %u" - args: ["x.address", "x.command"] - on_canalsat: - then: - - logger.log: - format: "on_canalsat: %u %u" - args: ["x.address", "x.command"] - # on_canalsatld: - # then: - # - logger.log: - # format: "on_canalsatld: %u %u" - # args: ["x.address", "x.command"] - on_coolix: - then: - - logger.log: - format: "on_coolix: %lu %lu" - args: ["long(x.first)", "long(x.second)"] - on_dish: - then: - - logger.log: - format: "on_dish: %u %u" - args: ["x.address", "x.command"] - on_dooya: - then: - - logger.log: - format: "on_dooya: %u %u %u" - args: ["x.channel", "x.button", "x.check"] - on_drayton: - then: - - logger.log: - format: "on_drayton: %u %u %u" - args: ["x.address", "x.channel", "x.command"] - on_jvc: - then: - - logger.log: - format: "on_jvc: %lu" - args: ["long(x.data)"] - on_keeloq: - then: - - logger.log: - format: "on_keeloq: %lu %lu %u" - args: ["long(x.encrypted)", "long(x.address)", "x.command"] - on_haier: - then: - - logger.log: - format: "on_haier: %u" - args: ["x.data.front()"] - on_lg: - then: - - logger.log: - format: "on_lg: %lu %u" - args: ["long(x.data)", "x.nbits"] - on_magiquest: - then: - - logger.log: - format: "on_magiquest: %u %lu" - args: ["x.magnitude", "long(x.wand_id)"] - on_midea: - then: - - logger.log: - format: "on_midea: %u %u" - args: ["x.size()", "x.data()[0]"] - on_nec: - then: - - logger.log: - format: "on_nec: %u %u" - args: ["x.address", "x.command"] - on_nexa: - then: - - logger.log: - format: "on_nexa: %lu %u %u %u %u" - args: ["long(x.device)", "x.group", "x.state", "x.channel", "x.level"] - on_panasonic: - then: - - logger.log: - format: "on_panasonic: %u %lu" - args: ["x.address", "long(x.command)"] - on_pioneer: - then: - - logger.log: - format: "on_pioneer: %u %u" - args: ["x.rc_code_1", "x.rc_code_2"] - on_pronto: - then: - - logger.log: - format: "on_pronto: %s" - args: ["x.data.c_str()"] - on_raw: - then: - - logger.log: - format: "on_raw: %lu" - args: ["long(x.front())"] - on_rc5: - then: - - logger.log: - format: "on_rc5: %u %u" - args: ["x.address", "x.command"] - on_rc6: - then: - - logger.log: - format: "on_rc6: %u %u" - args: ["x.address", "x.command"] - on_rc_switch: - then: - - logger.log: - format: "on_rc_switch: %llu %u" - args: ["x.code", "x.protocol"] - on_samsung: - then: - - logger.log: - format: "on_samsung: %llu %u" - args: ["x.data", "x.nbits"] - on_samsung36: - then: - - logger.log: - format: "on_samsung36: %u %lu" - args: ["x.address", "long(x.command)"] - on_sony: - then: - - logger.log: - format: "on_sony: %lu %u" - args: ["long(x.data)", "x.nbits"] - on_toshiba_ac: - then: - - logger.log: - format: "on_toshiba_ac: %llu %llu" - args: ["x.rc_code_1", "x.rc_code_2"] - on_mirage: - then: - - lambda: |- - ESP_LOGD("mirage", "Mirage data: %s", format_hex(x.data).c_str()); - -binary_sensor: - - platform: remote_receiver - name: Panasonic Remote Input - panasonic: - address: 0x4004 - command: 0x100BCBD diff --git a/tests/components/remote_receiver/test.esp32-ard.yaml b/tests/components/remote_receiver/test.esp32-ard.yaml index 16d276958a..5d29187206 100644 --- a/tests/components/remote_receiver/test.esp32-ard.yaml +++ b/tests/components/remote_receiver/test.esp32-ard.yaml @@ -3,4 +3,4 @@ substitutions: rmt_channel: "2" packages: - common: !include esp32-common.yaml + common: !include esp32-common-ard.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-ard.yaml b/tests/components/remote_receiver/test.esp32-c3-ard.yaml index 16d276958a..5d29187206 100644 --- a/tests/components/remote_receiver/test.esp32-c3-ard.yaml +++ b/tests/components/remote_receiver/test.esp32-c3-ard.yaml @@ -3,4 +3,4 @@ substitutions: rmt_channel: "2" packages: - common: !include esp32-common.yaml + common: !include esp32-common-ard.yaml diff --git a/tests/components/remote_receiver/test.esp32-c3-idf.yaml b/tests/components/remote_receiver/test.esp32-c3-idf.yaml index 16d276958a..495bb293c3 100644 --- a/tests/components/remote_receiver/test.esp32-c3-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-c3-idf.yaml @@ -1,6 +1,10 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + filter_symbols: "2" + receive_symbols: "4" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml index 16d276958a..495bb293c3 100644 --- a/tests/components/remote_receiver/test.esp32-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-idf.yaml @@ -1,6 +1,10 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + filter_symbols: "2" + receive_symbols: "4" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_receiver/test.esp32-s3-idf.yaml b/tests/components/remote_receiver/test.esp32-s3-idf.yaml index 265ecda771..e678ba456d 100644 --- a/tests/components/remote_receiver/test.esp32-s3-idf.yaml +++ b/tests/components/remote_receiver/test.esp32-s3-idf.yaml @@ -1,6 +1,10 @@ substitutions: pin: GPIO38 - rmt_channel: "5" + clock_resolution: "2000000" + filter_symbols: "2" + receive_symbols: "4" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_receiver/test.esp8266-ard.yaml b/tests/components/remote_receiver/test.esp8266-ard.yaml index 27d36d4a16..c9784ae003 100644 --- a/tests/components/remote_receiver/test.esp8266-ard.yaml +++ b/tests/components/remote_receiver/test.esp8266-ard.yaml @@ -2,150 +2,7 @@ remote_receiver: id: rcvr pin: GPIO5 dump: all - on_abbwelcome: - then: - - logger.log: - format: "on_abbwelcome: %u" - args: ["x.data()[0]"] - on_aeha: - then: - - logger.log: - format: "on_aeha: %u %u" - args: ["x.address", "x.data.front()"] - on_byronsx: - then: - - logger.log: - format: "on_byronsx: %u %u" - args: ["x.address", "x.command"] - on_canalsat: - then: - - logger.log: - format: "on_canalsat: %u %u" - args: ["x.address", "x.command"] - # on_canalsatld: - # then: - # - logger.log: - # format: "on_canalsatld: %u %u" - # args: ["x.address", "x.command"] - on_coolix: - then: - - logger.log: - format: "on_coolix: %u %u" - args: ["x.first", "x.second"] - on_dish: - then: - - logger.log: - format: "on_dish: %u %u" - args: ["x.address", "x.command"] - on_dooya: - then: - - logger.log: - format: "on_dooya: %u %u %u" - args: ["x.channel", "x.button", "x.check"] - on_drayton: - then: - - logger.log: - format: "on_drayton: %u %u %u" - args: ["x.address", "x.channel", "x.command"] - on_jvc: - then: - - logger.log: - format: "on_jvc: %u" - args: ["x.data"] - on_keeloq: - then: - - logger.log: - format: "on_keeloq: %u %u %u" - args: ["x.encrypted", "x.address", "x.command"] - on_haier: - then: - - logger.log: - format: "on_haier: %u" - args: ["x.data.front()"] - on_lg: - then: - - logger.log: - format: "on_lg: %u %u" - args: ["x.data", "x.nbits"] - on_magiquest: - then: - - logger.log: - format: "on_magiquest: %u %u" - args: ["x.magnitude", "x.wand_id"] - on_midea: - then: - - logger.log: - format: "on_midea: %u %u" - args: ["x.size()", "x.data()[0]"] - on_nec: - then: - - logger.log: - format: "on_nec: %u %u" - args: ["x.address", "x.command"] - on_nexa: - then: - - logger.log: - format: "on_nexa: %u %u %u %u %u" - args: ["x.device", "x.group", "x.state", "x.channel", "x.level"] - on_panasonic: - then: - - logger.log: - format: "on_panasonic: %u %u" - args: ["x.address", "x.command"] - on_pioneer: - then: - - logger.log: - format: "on_pioneer: %u %u" - args: ["x.rc_code_1", "x.rc_code_2"] - on_pronto: - then: - - logger.log: - format: "on_pronto: %s" - args: ["x.data.c_str()"] - on_raw: - then: - - logger.log: - format: "on_raw: %u" - args: ["x.front()"] - on_rc5: - then: - - logger.log: - format: "on_rc5: %u %u" - args: ["x.address", "x.command"] - on_rc6: - then: - - logger.log: - format: "on_rc6: %u %u" - args: ["x.address", "x.command"] - on_rc_switch: - then: - - logger.log: - format: "on_rc_switch: %llu %u" - args: ["x.code", "x.protocol"] - on_samsung: - then: - - logger.log: - format: "on_samsung: %llu %u" - args: ["x.data", "x.nbits"] - on_samsung36: - then: - - logger.log: - format: "on_samsung36: %u %u" - args: ["x.address", "x.command"] - on_sony: - then: - - logger.log: - format: "on_sony: %u %u" - args: ["x.data", "x.nbits"] - on_toshiba_ac: - then: - - logger.log: - format: "on_toshiba_ac: %llu %llu" - args: ["x.rc_code_1", "x.rc_code_2"] - on_mirage: - then: - - lambda: |- - ESP_LOGD("mirage", "Mirage data: %s", format_hex(x.data).c_str()); + <<: !include common-actions.yaml binary_sensor: - platform: remote_receiver diff --git a/tests/components/remote_transmitter/esp32-common-ard.yaml b/tests/components/remote_transmitter/esp32-common-ard.yaml new file mode 100644 index 0000000000..420cea326d --- /dev/null +++ b/tests/components/remote_transmitter/esp32-common-ard.yaml @@ -0,0 +1,8 @@ +remote_transmitter: + - id: xmitr + pin: ${pin} + rmt_channel: ${rmt_channel} + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/esp32-common-idf.yaml b/tests/components/remote_transmitter/esp32-common-idf.yaml new file mode 100644 index 0000000000..3b8b5e2aef --- /dev/null +++ b/tests/components/remote_transmitter/esp32-common-idf.yaml @@ -0,0 +1,11 @@ +remote_transmitter: + - id: xmitr + pin: ${pin} + carrier_duty_percent: 50% + clock_resolution: ${clock_resolution} + one_wire: ${one_wire} + rmt_symbols: ${rmt_symbols} + use_dma: ${use_dma} + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/esp32-common.yaml b/tests/components/remote_transmitter/esp32-common.yaml deleted file mode 100644 index 3f3cd3f8c7..0000000000 --- a/tests/components/remote_transmitter/esp32-common.yaml +++ /dev/null @@ -1,8 +0,0 @@ -remote_transmitter: - id: rcvr - pin: ${pin} - rmt_channel: ${rmt_channel} - carrier_duty_percent: 50% - -packages: - buttons: !include common-buttons.yaml diff --git a/tests/components/remote_transmitter/test.esp32-ard.yaml b/tests/components/remote_transmitter/test.esp32-ard.yaml index 16d276958a..5d29187206 100644 --- a/tests/components/remote_transmitter/test.esp32-ard.yaml +++ b/tests/components/remote_transmitter/test.esp32-ard.yaml @@ -3,4 +3,4 @@ substitutions: rmt_channel: "2" packages: - common: !include esp32-common.yaml + common: !include esp32-common-ard.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml index 3e2dc88e5a..c755b11563 100644 --- a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml +++ b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml @@ -3,4 +3,4 @@ substitutions: rmt_channel: "1" packages: - common: !include esp32-common.yaml + common: !include esp32-common-ard.yaml diff --git a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml index 3e2dc88e5a..1a27f29dac 100644 --- a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml @@ -1,6 +1,9 @@ substitutions: pin: GPIO2 - rmt_channel: "1" + clock_resolution: "2000000" + one_wire: "true" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_transmitter/test.esp32-idf.yaml b/tests/components/remote_transmitter/test.esp32-idf.yaml index 16d276958a..1a27f29dac 100644 --- a/tests/components/remote_transmitter/test.esp32-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-idf.yaml @@ -1,6 +1,9 @@ substitutions: pin: GPIO2 - rmt_channel: "2" + clock_resolution: "2000000" + one_wire: "true" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml index 31851dc54c..25bdbd4772 100644 --- a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml +++ b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml @@ -1,6 +1,9 @@ substitutions: pin: GPIO38 - rmt_channel: "3" + clock_resolution: "2000000" + one_wire: "true" + rmt_symbols: "64" + use_dma: "true" packages: - common: !include esp32-common.yaml + common: !include esp32-common-idf.yaml diff --git a/tests/components/remote_transmitter/test.esp8266-ard.yaml b/tests/components/remote_transmitter/test.esp8266-ard.yaml index de494485f4..19759360f4 100644 --- a/tests/components/remote_transmitter/test.esp8266-ard.yaml +++ b/tests/components/remote_transmitter/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ remote_transmitter: - id: trns + id: xmitr pin: GPIO5 carrier_duty_percent: 50% diff --git a/tests/components/seeed_mr60bha2/common.yaml b/tests/components/seeed_mr60bha2/common.yaml new file mode 100644 index 0000000000..e9d0c735af --- /dev/null +++ b/tests/components/seeed_mr60bha2/common.yaml @@ -0,0 +1,19 @@ +uart: + - id: seeed_mr60fda2_uart + tx_pin: ${uart_tx_pin} + rx_pin: ${uart_rx_pin} + baud_rate: 115200 + parity: NONE + stop_bits: 1 + +seeed_mr60bha2: + id: my_seeed_mr60bha2 + +sensor: + - platform: seeed_mr60bha2 + breath_rate: + name: "Real-time respiratory rate" + heart_rate: + name: "Real-time heart rate" + distance: + name: "Distance to detection object" diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..4fb884abf4 --- /dev/null +++ b/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO5 + uart_rx_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..4fb884abf4 --- /dev/null +++ b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + uart_tx_pin: GPIO5 + uart_rx_pin: GPIO4 + +<<: !include common.yaml 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 diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 34f70be2fb..93ae67754a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,12 +1,12 @@ -import pytest import string -from hypothesis import given, example -from hypothesis.strategies import one_of, text, integers, builds +from hypothesis import example, given +from hypothesis.strategies import builds, integers, ip_addresses, one_of, text +import pytest from esphome import config_validation from esphome.config_validation import Invalid -from esphome.core import CORE, Lambda, HexInt +from esphome.core import CORE, HexInt, Lambda def test_check_not_templatable__invalid(): @@ -145,6 +145,28 @@ def test_boolean__invalid(value): config_validation.boolean(value) +@given(value=ip_addresses(v=4).map(str)) +def test_ipv4__valid(value): + config_validation.ipv4address(value) + + +@pytest.mark.parametrize("value", ("127.0.0", "localhost", "")) +def test_ipv4__invalid(value): + with pytest.raises(Invalid, match="is not a valid IPv4 address"): + config_validation.ipv4address(value) + + +@given(value=ip_addresses(v=6).map(str)) +def test_ipv6__valid(value): + config_validation.ipaddress(value) + + +@pytest.mark.parametrize("value", ("127.0.0", "localhost", "", "2001:db8::2::3")) +def test_ipv6__invalid(value): + with pytest.raises(Invalid, match="is not a valid IP address"): + config_validation.ipaddress(value) + + # TODO: ensure_list @given(integers()) def hex_int__valid(value): diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 2860486efe..4f2a6453b4 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,10 +1,8 @@ -import pytest - from hypothesis import given -from hypothesis.strategies import ip_addresses +import pytest from strategies import mac_addr_strings -from esphome import core, const +from esphome import const, core class TestHexInt: @@ -26,25 +24,6 @@ class TestHexInt: assert actual == expected -class TestIPAddress: - @given(value=ip_addresses(v=4).map(str)) - def test_init__valid(self, value): - core.IPAddress(*value.split(".")) - - @pytest.mark.parametrize("value", ("127.0.0", "localhost", "")) - def test_init__invalid(self, value): - with pytest.raises(ValueError, match="IPAddress must consist of 4 items"): - core.IPAddress(*value.split(".")) - - @given(value=ip_addresses(v=4).map(str)) - def test_str(self, value): - target = core.IPAddress(*value.split(".")) - - actual = str(target) - - assert actual == value - - class TestMACAddress: @given(value=mac_addr_strings()) def test_init__valid(self, value):