diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 9fb80e6a9d..5d290894a7 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index c42b5330d2..66369c706f 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -32,7 +32,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - name: Auto Label PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index c7cc720323..ec214d1a77 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" @@ -47,7 +47,7 @@ jobs: fi - if: failure() name: Review PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -70,7 +70,7 @@ jobs: esphome/components/api/api_pb2_service.* - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index c7da7f6672..2f47386abf 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" @@ -41,7 +41,7 @@ jobs: - if: failure() name: Request changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -54,7 +54,7 @@ jobs: - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 61ecf8183b..915a4dfb7e 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f429f7b40..07fd91b1c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment @@ -156,7 +156,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache @@ -217,7 +217,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python 3.13 id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.13" - name: Restore Python virtual environment diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index ab3377365d..475e05b970 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Request reviews from component codeowners - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 29103e8eee..736c986f7e 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add external component comment - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 3639d346f5..ab9b96b45a 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify codeowners for component issues - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9af67aa310..efc8424cd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.x" - name: Build @@ -70,7 +70,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true @@ -94,7 +94,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" @@ -220,7 +220,7 @@ jobs: - deploy-manifest steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | @@ -246,7 +246,7 @@ jobs: environment: ${{ needs.init.outputs.deploy_env }} steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} script: | diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b79939fc8e..88e07d3f58 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@v10.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -37,7 +37,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@v10.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index 157f60f3a1..675be49c27 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -16,7 +16,7 @@ jobs: - merge-after-release steps: - name: Check for ${{ matrix.label }} label - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index cc03ed3e3f..b129e8f4bf 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: 3.13 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e05733ec96..2b161cf05c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.11 + rev: v0.12.12 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 116f35f3b6..a77a7ba86e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,7 @@ esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow -esphome/components/camera/* @DT-art1 @bdraco +esphome/components/camera/* @bdraco @DT-art1 esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 @@ -145,9 +145,9 @@ esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8388/* @P4uLT esphome/components/esp32/* @esphome/core -esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz +esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito esphome/components/esp32_ble_client/* @bdraco @jesserockz -esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz +esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron @@ -167,7 +167,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi -esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh +esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt esphome/components/ft5x06/* @clydebarrow @@ -203,7 +203,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 -esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 @@ -228,7 +228,7 @@ esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core -esphome/components/ina226/* @Sergio303 @latonita +esphome/components/ina226/* @latonita @Sergio303 esphome/components/ina260/* @mreditor97 esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita @@ -277,8 +277,8 @@ esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz -esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz -esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz +esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw +esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw esphome/components/mcp23x08_base/* @jesserockz esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz @@ -299,6 +299,7 @@ esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mipi_dsi/* @clydebarrow +esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt @@ -342,7 +343,7 @@ esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow esphome/components/pca6416a/* @Mat931 -esphome/components/pca9554/* @clydebarrow @hwstar +esphome/components/pca9554/* @bdraco @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman esphome/components/pi4ioe5v6408/* @jesserockz @@ -353,9 +354,9 @@ esphome/components/pm2005/* @andrewjswan esphome/components/pmsa003i/* @sjtrny esphome/components/pmsx003/* @ximex esphome/components/pmwcs3/* @SeByDocKy -esphome/components/pn532/* @OttoWinter @jesserockz -esphome/components/pn532_i2c/* @OttoWinter @jesserockz -esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn532/* @jesserockz @OttoWinter +esphome/components/pn532_i2c/* @jesserockz @OttoWinter +esphome/components/pn532_spi/* @jesserockz @OttoWinter esphome/components/pn7150/* @jesserockz @kbx81 esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 @@ -364,7 +365,7 @@ esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter +esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston esphome/components/pvvx_mithermometer/* @pasiz esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc @@ -405,7 +406,7 @@ esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw -esphome/components/sgp4x/* @SenexCrenshaw @martgras +esphome/components/sgp4x/* @martgras @SenexCrenshaw esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht3xd/* @mrtoy-me esphome/components/sht4x/* @sjtrny diff --git a/esphome/__main__.py b/esphome/__main__.py index aab3035a5e..280f491924 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -396,27 +396,29 @@ def check_permissions(port: str): ) -def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str: +def upload_program( + config: ConfigType, args: ArgsProtocol, devices: list[str] +) -> tuple[int, str | None]: + host = devices[0] try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): - return 0 + return 0, host except AttributeError: pass if get_port_type(host) == "SERIAL": check_permissions(host) + + exit_code = 1 if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): file = getattr(args, "file", None) - return upload_using_esptool(config, host, file, args.upload_speed) + exit_code = upload_using_esptool(config, host, file, args.upload_speed) + elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny: + exit_code = upload_using_platformio(config, host) + # else: Unknown target platform, exit_code remains 1 - if CORE.target_platform in (PLATFORM_RP2040): - return upload_using_platformio(config, host) - - if CORE.is_libretiny: - return upload_using_platformio(config, host) - - return 1 # Unknown target platform + return exit_code, host if exit_code == 0 else None ota_conf = {} for ota_item in config.get(CONF_OTA, []): @@ -433,10 +435,10 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD, "") + binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin # Check if we should use MQTT for address resolution # This happens when no device was specified, or the current host is "MQTT"/"OTA" - devices: list[str] = args.device or [] if ( CONF_MQTT in config # pylint: disable=too-many-boolean-expressions and (not devices or host in ("MQTT", "OTA")) @@ -447,17 +449,23 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s ): from esphome import mqtt - host = mqtt.get_esphome_device_ip( - config, args.username, args.password, args.client_id - ) + devices = [ + mqtt.get_esphome_device_ip( + config, args.username, args.password, args.client_id + ) + ] - if getattr(args, "file", None) is not None: - return espota2.run_ota(host, remote_port, password, args.file) - - return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) + return espota2.run_ota(devices, remote_port, password, binary) def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + if getattr(module, "show_logs")(config, args, devices): + return 0 + except AttributeError: + pass + if "logger" not in config: raise EsphomeError("Logger is not configured!") @@ -551,17 +559,11 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - # Try each device until one succeeds - exit_code = 1 - for device in devices: - _LOGGER.info("Uploading to %s", device) - exit_code = upload_program(config, args, device) - if exit_code == 0: - _LOGGER.info("Successfully uploaded program.") - return 0 - if len(devices) > 1: - _LOGGER.warning("Failed to upload to %s", device) - + exit_code, _ = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code @@ -614,19 +616,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - # Try each device for upload until one succeeds - successful_device: str | None = None - for device in devices: - _LOGGER.info("Uploading to %s", device) - exit_code = upload_program(config, args, device) - if exit_code == 0: - _LOGGER.info("Successfully uploaded program.") - successful_device = device - break - if len(devices) > 1: - _LOGGER.warning("Failed to upload to %s", device) - - if successful_device is None: + exit_code, successful_device = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code if args.no_logs: diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 7ba3c5a1ab..2c5603ee3d 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -64,7 +64,7 @@ void AbsoluteHumidityComponent::loop() { ESP_LOGW(TAG, "No valid state from humidity sensor!"); } this->publish_state(NAN); - this->status_set_warning("Unable to calculate absolute humidity."); + this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); return; } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 87d4ddd35f..ab6a89fce0 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -241,6 +241,8 @@ float ADCSensor::sample_autorange_() { cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #else adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, @@ -251,10 +253,14 @@ float ADCSensor::sample_autorange_() { #endif }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #endif int raw; err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); + ESP_LOGVV(TAG, "Autorange atten=%d: Raw ADC read %s, value=%d (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", raw, err); if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); @@ -275,8 +281,10 @@ float ADCSensor::sample_autorange_() { err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); if (err == ESP_OK) { voltage = voltage_mv / 1000.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: CALIBRATED - raw=%d -> %dmV -> %.6fV", atten, raw, voltage_mv, voltage); } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ @@ -287,6 +295,7 @@ float ADCSensor::sample_autorange_() { #endif } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: NO CALIBRATION - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } return {raw, voltage}; @@ -324,18 +333,32 @@ float ADCSensor::sample_autorange_() { } const int adc_half = 2048; - 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(4095 - raw0, adc_half); - uint32_t csum = c12 + c6 + c2 + c0; + const uint32_t c12 = std::min(raw12, adc_half); + + const int32_t c6_signed = adc_half - std::abs(raw6 - adc_half); + const uint32_t c6 = (c6_signed > 0) ? c6_signed : 0; // Clamp to prevent underflow + + const int32_t c2_signed = adc_half - std::abs(raw2 - adc_half); + const uint32_t c2 = (c2_signed > 0) ? c2_signed : 0; // Clamp to prevent underflow + + const uint32_t c0 = std::min(4095 - raw0, adc_half); + const uint32_t csum = c12 + c6 + c2 + c0; + + ESP_LOGVV(TAG, "Autorange summary:"); + ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); + ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); + ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); if (csum == 0) { ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); return NAN; } - return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, + c0, csum, final_result); + + return final_result; } } // namespace adc diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 6202a27c42..53c712a7a7 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -96,7 +96,7 @@ void AHT10Component::read_data_() { ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); } if (this->read(data, 6) != i2c::ERROR_OK) { - this->status_set_warning("Read failed, will retry"); + this->status_set_warning(LOG_STR("Read failed, will retry")); this->restart_read_(); return; } @@ -113,7 +113,7 @@ void AHT10Component::read_data_() { } else { ESP_LOGD(TAG, "Invalid humidity, retrying"); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); } this->restart_read_(); return; @@ -144,7 +144,7 @@ void AHT10Component::update() { return; this->start_time_ = millis(); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->restart_read_(); diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 1d10474e17..7b2a0665d7 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -825,6 +825,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; + string timezone = 2; } // ==================== USER-DEFINES SERVICES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c82d39e9f7..aceb0a9da1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -115,7 +115,7 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Helper init failed", err); + this->log_warning_(LOG_STR("Helper init failed"), err); return; } this->client_info_.peername = helper_->getpeername(); @@ -162,7 +162,7 @@ void APIConnection::loop() { break; } else if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Reading failed", err); + this->log_warning_(LOG_STR("Reading failed"), err); return; } else { this->last_traffic_ = now; @@ -1073,8 +1073,14 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { #ifdef USE_HOMEASSISTANT_TIME void APIConnection::on_get_time_response(const GetTimeResponse &value) { - if (homeassistant::global_homeassistant_time != nullptr) + if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); +#ifdef USE_TIME_TIMEZONE + if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) { + homeassistant::global_homeassistant_time->set_timezone(value.timezone); + } +#endif + } } #endif @@ -1584,7 +1590,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Packet write failed", err); + this->log_warning_(LOG_STR("Packet write failed"), err); return false; } // Do not set last_traffic_ on send @@ -1771,7 +1777,7 @@ void APIConnection::process_batch_() { std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); - this->log_warning_("Batch write failed", err); + this->log_warning_(LOG_STR("Batch write failed"), err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1849,11 +1855,14 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES -void APIConnection::log_warning_(const char *message, APIError err) { - ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno); +void APIConnection::log_warning_(const LogString *message, APIError err) { + ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message), + LOG_STR_ARG(api_error_to_logstr(err)), errno); } -void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); } +void APIConnection::log_socket_operation_failed_(APIError err) { + this->log_warning_(LOG_STR("Socket operation failed"), err); +} } // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 94f12d13b6..64547b84da 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -738,7 +738,7 @@ class APIConnection final : public APIServerConnection { } // Helper function to log API errors with errno - void log_warning_(const char *message, APIError err); + void log_warning_(const LogString *message, APIError err); // Specific helper for duplicated error message void log_socket_operation_failed_(APIError err); }; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index dee3af2ac3..a284e09c4a 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -23,59 +23,59 @@ static const char *const TAG = "api.frame_helper"; #define LOG_PACKET_SENDING(data, len) ((void) 0) #endif -const char *api_error_to_str(APIError err) { +const LogString *api_error_to_logstr(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it if (err == APIError::OK) { - return "OK"; + return LOG_STR("OK"); } else if (err == APIError::WOULD_BLOCK) { - return "WOULD_BLOCK"; + return LOG_STR("WOULD_BLOCK"); } else if (err == APIError::BAD_INDICATOR) { - return "BAD_INDICATOR"; + return LOG_STR("BAD_INDICATOR"); } else if (err == APIError::BAD_DATA_PACKET) { - return "BAD_DATA_PACKET"; + return LOG_STR("BAD_DATA_PACKET"); } else if (err == APIError::TCP_NODELAY_FAILED) { - return "TCP_NODELAY_FAILED"; + return LOG_STR("TCP_NODELAY_FAILED"); } else if (err == APIError::TCP_NONBLOCKING_FAILED) { - return "TCP_NONBLOCKING_FAILED"; + return LOG_STR("TCP_NONBLOCKING_FAILED"); } else if (err == APIError::CLOSE_FAILED) { - return "CLOSE_FAILED"; + return LOG_STR("CLOSE_FAILED"); } else if (err == APIError::SHUTDOWN_FAILED) { - return "SHUTDOWN_FAILED"; + return LOG_STR("SHUTDOWN_FAILED"); } else if (err == APIError::BAD_STATE) { - return "BAD_STATE"; + return LOG_STR("BAD_STATE"); } else if (err == APIError::BAD_ARG) { - return "BAD_ARG"; + return LOG_STR("BAD_ARG"); } else if (err == APIError::SOCKET_READ_FAILED) { - return "SOCKET_READ_FAILED"; + return LOG_STR("SOCKET_READ_FAILED"); } else if (err == APIError::SOCKET_WRITE_FAILED) { - return "SOCKET_WRITE_FAILED"; + return LOG_STR("SOCKET_WRITE_FAILED"); } else if (err == APIError::OUT_OF_MEMORY) { - return "OUT_OF_MEMORY"; + return LOG_STR("OUT_OF_MEMORY"); } else if (err == APIError::CONNECTION_CLOSED) { - return "CONNECTION_CLOSED"; + return LOG_STR("CONNECTION_CLOSED"); } #ifdef USE_API_NOISE else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { - return "BAD_HANDSHAKE_PACKET_LEN"; + return LOG_STR("BAD_HANDSHAKE_PACKET_LEN"); } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { - return "HANDSHAKESTATE_READ_FAILED"; + return LOG_STR("HANDSHAKESTATE_READ_FAILED"); } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { - return "HANDSHAKESTATE_WRITE_FAILED"; + return LOG_STR("HANDSHAKESTATE_WRITE_FAILED"); } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { - return "HANDSHAKESTATE_BAD_STATE"; + return LOG_STR("HANDSHAKESTATE_BAD_STATE"); } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { - return "CIPHERSTATE_DECRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_DECRYPT_FAILED"); } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { - return "CIPHERSTATE_ENCRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { - return "HANDSHAKESTATE_SETUP_FAILED"; + return LOG_STR("HANDSHAKESTATE_SETUP_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { - return "HANDSHAKESTATE_SPLIT_FAILED"; + return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED"); } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { - return "BAD_HANDSHAKE_ERROR_BYTE"; + return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); } #endif - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } // Default implementation for loop - handles sending buffered data diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 43e9d95fbe..c11d701ffe 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -66,7 +66,7 @@ enum class APIError : uint16_t { #endif }; -const char *api_error_to_str(APIError err); +const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 35d1715931..0e49f93db5 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -10,10 +10,18 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.noise"; +#ifdef USE_ESP8266 +static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +#else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; +#endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) @@ -27,42 +35,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #endif /// Convert a noise error code to a readable error -std::string noise_err_to_str(int err) { +const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) - return "NO_MEMORY"; + return LOG_STR("NO_MEMORY"); if (err == NOISE_ERROR_UNKNOWN_ID) - return "UNKNOWN_ID"; + return LOG_STR("UNKNOWN_ID"); if (err == NOISE_ERROR_UNKNOWN_NAME) - return "UNKNOWN_NAME"; + return LOG_STR("UNKNOWN_NAME"); if (err == NOISE_ERROR_MAC_FAILURE) - return "MAC_FAILURE"; + return LOG_STR("MAC_FAILURE"); if (err == NOISE_ERROR_NOT_APPLICABLE) - return "NOT_APPLICABLE"; + return LOG_STR("NOT_APPLICABLE"); if (err == NOISE_ERROR_SYSTEM) - return "SYSTEM"; + return LOG_STR("SYSTEM"); if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return "REMOTE_KEY_REQUIRED"; + return LOG_STR("REMOTE_KEY_REQUIRED"); if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return "LOCAL_KEY_REQUIRED"; + return LOG_STR("LOCAL_KEY_REQUIRED"); if (err == NOISE_ERROR_PSK_REQUIRED) - return "PSK_REQUIRED"; + return LOG_STR("PSK_REQUIRED"); if (err == NOISE_ERROR_INVALID_LENGTH) - return "INVALID_LENGTH"; + return LOG_STR("INVALID_LENGTH"); if (err == NOISE_ERROR_INVALID_PARAM) - return "INVALID_PARAM"; + return LOG_STR("INVALID_PARAM"); if (err == NOISE_ERROR_INVALID_STATE) - return "INVALID_STATE"; + return LOG_STR("INVALID_STATE"); if (err == NOISE_ERROR_INVALID_NONCE) - return "INVALID_NONCE"; + return LOG_STR("INVALID_NONCE"); if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return "INVALID_PRIVATE_KEY"; + return LOG_STR("INVALID_PRIVATE_KEY"); if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return "INVALID_PUBLIC_KEY"; + return LOG_STR("INVALID_PUBLIC_KEY"); if (err == NOISE_ERROR_INVALID_FORMAT) - return "INVALID_FORMAT"; + return LOG_STR("INVALID_FORMAT"); if (err == NOISE_ERROR_INVALID_SIGNATURE) - return "INVALID_SIGNATURE"; - return to_string(err); + return LOG_STR("INVALID_SIGNATURE"); + return LOG_STR("UNKNOWN"); } /// Initialize the frame helper, returns OK if successful. @@ -75,7 +83,11 @@ APIError APINoiseFrameHelper::init() { // init prologue size_t old_size = prologue_.size(); prologue_.resize(old_size + PROLOGUE_INIT_LEN); +#ifdef USE_ESP8266 + memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#else std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#endif state_ = State::CLIENT_HELLO; return APIError::OK; @@ -83,18 +95,18 @@ APIError APINoiseFrameHelper::init() { // Helper for handling handshake frame errors APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); + send_explicit_handshake_reject_(LOG_STR("Bad indicator byte")); } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); + send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len")); } return aerr; } // Helper for handling noise library errors -APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { +APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) { if (err != 0) { state_ = State::FAILED; - HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err))); return api_err; } return APIError::OK; @@ -279,11 +291,11 @@ APIError APINoiseFrameHelper::state_action_() { } if (frame.empty()) { - send_explicit_handshake_reject_("Empty handshake message"); + send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } else if (frame[0] != 0x00) { HELPER_LOG("Bad handshake error byte: %u", frame[0]); - send_explicit_handshake_reject_("Bad handshake error byte"); + send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } @@ -293,8 +305,10 @@ APIError APINoiseFrameHelper::state_action_() { err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); - return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") + : LOG_STR("Handshake error")); + return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), + APIError::HANDSHAKESTATE_READ_FAILED); } aerr = check_handshake_finished_(); @@ -307,8 +321,8 @@ APIError APINoiseFrameHelper::state_action_() { noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = - handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), + APIError::HANDSHAKESTATE_WRITE_FAILED); if (aerr_write != APIError::OK) return aerr_write; buffer[0] = 0x00; // success @@ -331,15 +345,31 @@ APIError APINoiseFrameHelper::state_action_() { } return APIError::OK; } -void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { +void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + // On ESP8266 with flash strings, we need to use PROGMEM-aware functions + size_t reason_len = strlen_P(reinterpret_cast(reason)); std::vector data; - data.resize(reason.length() + 1); + data.resize(reason_len + 1); + data[0] = 0x01; // failure + + // Copy error message from PROGMEM + if (reason_len > 0) { + memcpy_P(data.data() + 1, reinterpret_cast(reason), reason_len); + } +#else + // Normal memory access + const char *reason_str = LOG_STR_ARG(reason); + size_t reason_len = strlen(reason_str); + std::vector data; + data.resize(reason_len + 1); data[0] = 0x01; // failure // Copy error message in bulk - if (!reason.empty()) { - std::memcpy(data.data() + 1, reason.c_str(), reason.length()); + if (reason_len > 0) { + std::memcpy(data.data() + 1, reason_str, reason_len); } +#endif // temporarily remove failed state auto orig_state = state_; @@ -368,7 +398,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { noise_buffer_init(mbuf); noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); + APIError decrypt_err = + handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); if (decrypt_err != APIError::OK) return decrypt_err; @@ -450,7 +481,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); if (aerr != APIError::OK) return aerr; @@ -504,25 +536,27 @@ APIError APINoiseFrameHelper::init_handshake_() { nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; const auto &psk = ctx_->get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), + APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now prologue_ = {}; err = noise_handshakestate_start(handshake_); - aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; return APIError::OK; @@ -540,7 +574,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_BAD_STATE; } int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 49bc6f8854..71a217c4ca 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -32,9 +32,9 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); APIError check_handshake_finished_(); - void send_explicit_handshake_reject_(const std::string &reason); + void send_explicit_handshake_reject_(const LogString *reason); APIError handle_handshake_frame_error_(APIError aerr); - APIError handle_noise_error_(int err, const char *func_name, APIError api_err); + APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); // Pointers first (4 bytes each) NoiseHandshakeState *handshake_{nullptr}; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index fdaacbd94e..859bb26630 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -10,6 +10,10 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.plaintext"; @@ -197,11 +201,20 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { // We must send at least 3 bytes to be read, so we add // a message after the indicator byte to ensures its long // enough and can aid in debugging. - const char msg[] = "\x00" - "Bad indicator byte"; + static constexpr uint8_t INDICATOR_MSG_SIZE = 19; +#ifdef USE_ESP8266 + static const char MSG_PROGMEM[] PROGMEM = "\x00" + "Bad indicator byte"; + char msg[INDICATOR_MSG_SIZE]; + memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); iov[0].iov_base = (void *) msg; - iov[0].iov_len = 19; - this->write_raw_(iov, 1, 19); +#else + static const char MSG[] = "\x00" + "Bad indicator byte"; + iov[0].iov_base = (void *) MSG; +#endif + iov[0].iov_len = INDICATOR_MSG_SIZE; + this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); } return aerr; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index cdb941cc59..e4ad8d591b 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -907,6 +907,16 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: + this->timezone = value.as_string(); + break; + default: + return false; + } + return true; +} bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: @@ -917,8 +927,14 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } -void GetTimeResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->epoch_seconds); } +void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->epoch_seconds); + buffer.encode_string(2, this->timezone_ref_); +} +void GetTimeResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->epoch_seconds); + size.add_length(1, this->timezone_ref_.size()); +} #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 61fbb733b9..581e514ee5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1177,11 +1177,14 @@ class GetTimeRequest final : public ProtoMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 5; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; + std::string timezone{}; + StringRef timezone_ref_{}; + void set_timezone(const StringRef &ref) { this->timezone_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1190,6 +1193,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; #ifdef USE_API_SERVICES class ListEntitiesServicesArgument final : public ProtoMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a8344771a9..0d18f4f51b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1113,7 +1113,17 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { } #endif void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } +void GetTimeResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "GetTimeResponse"); + dump_field(out, "epoch_seconds", this->epoch_seconds); + out.append(" timezone: "); + if (!this->timezone_ref_.empty()) { + out.append("'").append(this->timezone_ref_.c_str()).append("'"); + } else { + out.append("'").append(this->timezone).append("'"); + } + out.append("\n"); +} #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index 42ef259100..4522e94846 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_LIGHTBULB, @@ -78,6 +79,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 7cdbd69f56..a510095217 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -17,10 +17,12 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, @@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 6304516164..ab3f1dad4f 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -12,7 +12,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning("Failed to communicate"); \ + this->status_set_warning(LOG_STR("Failed to communicate")); \ return; \ } diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 007ca1ca7d..38fcf29b3b 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -493,7 +493,7 @@ void BedJetHub::dump_config() { " ble_client.app_id: %d\n" " ble_client.conn_id: %d", this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id()); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size()); for (auto *child : this->children_) { ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str()); diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 86eff57147..894fcbfbb7 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -149,7 +149,7 @@ void BL0942::setup() { this->write_reg_(BL0942_REG_USR_WRPROT, 0); if (this->read_reg_(BL0942_REG_MODE) != mode) - this->status_set_warning("BL0942 setup failed!"); + this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->flush(); } diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 25179fdacc..7eb0ffa99e 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -11,17 +11,35 @@ namespace captive_portal { static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("application/json"); - stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); + AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); + stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); +#ifdef USE_ESP8266 + stream->print(F("{\"mac\":\"")); + stream->print(get_mac_address_pretty().c_str()); + stream->print(F("\",\"name\":\"")); + stream->print(App.get_name().c_str()); + stream->print(F("\",\"aps\":[{}")); +#else stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); +#endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { if (scan.get_is_hidden()) continue; - // Assumes no " in ssid, possible unicode isses? + // Assumes no " in ssid, possible unicode isses? +#ifdef USE_ESP8266 + stream->print(F(",{\"ssid\":\"")); + stream->print(scan.get_ssid().c_str()); + stream->print(F("\",\"rssi\":")); + stream->print(scan.get_rssi()); + stream->print(F(",\"lock\":")); + stream->print(scan.get_with_auth()); + stream->print(F("}")); +#else stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), scan.get_with_auth()); +#endif } stream->print(F("]}")); request->send(stream); @@ -34,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect("/?save"); + request->redirect(F("/?save")); } void CaptivePortal::setup() { @@ -53,18 +71,23 @@ void CaptivePortal::start() { this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", ip); + this->dns_server_->start(53, F("*"), ip); // Re-enable loop() when DNS server is started this->enable_loop(); #endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { - req->send(404, "text/html", "File not found"); + req->send(404, F("text/html"), F("File not found")); return; } +#ifdef USE_ESP8266 + String url = F("http://"); + url += wifi::global_wifi_component->wifi_soft_ap_ip().str().c_str(); +#else auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); +#endif req->redirect(url.c_str()); }); @@ -73,19 +96,19 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == "/") { + if (req->url() == F("/")) { #ifndef USE_ESP8266 - auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #else - auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(F("Content-Encoding"), F("gzip")); req->send(response); return; - } else if (req->url() == "/config.json") { + } else if (req->url() == F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == "/wifisave") { + } else if (req->url() == F("/wifisave")) { this->handle_wifisave(req); return; } diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c78fff824a..382afe92f0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -45,11 +45,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { return false; if (request->method() == HTTP_GET) { - if (request->url() == "/") + if (request->url() == F("/")) return true; - if (request->url() == "/config.json") + if (request->url() == F("/config.json")) return true; - if (request->url() == "/wifisave") + if (request->url() == F("/wifisave")) return true; } diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 2617d7577a..40c5318339 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -152,7 +152,7 @@ void CCS811Component::send_env_data_() { void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2 Sensor", this->co2_); LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 5cd6063893..a518c96489 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -64,7 +64,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } } else { ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); - this->status_set_warning("bus reset failed"); + this->status_set_warning(LOG_STR("bus reset failed")); } return success; } @@ -124,7 +124,7 @@ bool DallasTemperatureSensor::check_scratch_pad_() { crc8(this->scratch_pad_, 8)); #endif if (!chksum_validity) { - this->status_set_warning("scratch pad checksum invalid"); + this->status_set_warning(LOG_STR("scratch pad checksum invalid")); ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 37990aeec5..b1dfe1bc9a 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -52,7 +52,7 @@ void DebugComponent::on_shutdown() { char buffer[REBOOT_MAX_LEN]{}; auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); if (component != nullptr) { - strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); + strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1); buffer[REBOOT_MAX_LEN - 1] = '\0'; } ESP_LOGD(TAG, "Storing reboot source: %s", buffer); diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 27572063ca..a98245b889 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -54,13 +54,13 @@ struct ISRPinArg { ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = this->pin_; + arg->pin = this->get_pin_num(); arg->flags = gpio::FLAG_NONE; - arg->inverted = inverted_; + arg->inverted = this->pin_flags_.inverted; #if defined(USE_ESP32_VARIANT_ESP32) - arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_); + arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num()); if (arg->use_rtc) - arg->rtc_pin = rtc_io_number_get(this->pin_); + arg->rtc_pin = rtc_io_number_get(this->get_pin_num()); #endif return ISRInternalGPIOPin((void *) arg); } @@ -69,23 +69,23 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { case gpio::INTERRUPT_RISING_EDGE: - idf_type = inverted_ ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; break; case gpio::INTERRUPT_FALLING_EDGE: - idf_type = inverted_ ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; break; case gpio::INTERRUPT_ANY_EDGE: idf_type = GPIO_INTR_ANYEDGE; break; case gpio::INTERRUPT_LOW_LEVEL: - idf_type = inverted_ ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; break; case gpio::INTERRUPT_HIGH_LEVEL: - idf_type = inverted_ ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; break; } - gpio_set_intr_type(pin_, idf_type); - gpio_intr_enable(pin_); + gpio_set_intr_type(this->get_pin_num(), idf_type); + gpio_intr_enable(this->get_pin_num()); if (!isr_service_installed) { auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); if (res != ESP_OK) { @@ -94,31 +94,31 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi } isr_service_installed = true; } - gpio_isr_handler_add(pin_, func, arg); + gpio_isr_handler_add(this->get_pin_num(), func, arg); } std::string ESP32InternalGPIOPin::dump_summary() const { char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(pin_)); + snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(this->pin_)); return buffer; } void ESP32InternalGPIOPin::setup() { gpio_config_t conf{}; - conf.pin_bit_mask = 1ULL << static_cast(pin_); - conf.mode = flags_to_mode(flags_); - conf.pull_up_en = flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + conf.pin_bit_mask = 1ULL << static_cast(this->pin_); + conf.mode = flags_to_mode(this->flags_); + conf.pull_up_en = this->flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + conf.pull_down_en = this->flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&conf); - if (flags_ & gpio::FLAG_OUTPUT) { - gpio_set_drive_capability(pin_, drive_strength_); + if (this->flags_ & gpio::FLAG_OUTPUT) { + gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength()); } } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { // can't call gpio_config here because that logs in esp-idf which may cause issues - gpio_set_direction(pin_, flags_to_mode(flags)); + gpio_set_direction(this->get_pin_num(), flags_to_mode(flags)); gpio_pull_mode_t pull_mode = GPIO_FLOATING; if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; @@ -127,12 +127,16 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { } else if (flags & gpio::FLAG_PULLDOWN) { pull_mode = GPIO_PULLDOWN_ONLY; } - gpio_set_pull_mode(pin_, pull_mode); + gpio_set_pull_mode(this->get_pin_num(), pull_mode); } -bool ESP32InternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } -void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } -void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } +bool ESP32InternalGPIOPin::digital_read() { + return bool(gpio_get_level(this->get_pin_num())) != this->pin_flags_.inverted; +} +void ESP32InternalGPIOPin::digital_write(bool value) { + gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0); +} +void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); } } // namespace esp32 diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 0fefc1c058..565e276ea8 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -7,12 +7,18 @@ namespace esphome { namespace esp32 { +// Static assertions to ensure our bit-packed fields can hold the enum values +static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t"); +static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field"); + class ESP32InternalGPIOPin : public InternalGPIOPin { public: - void set_pin(gpio_num_t pin) { pin_ = pin; } - void set_inverted(bool inverted) { inverted_ = inverted; } - void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; } - void set_flags(gpio::Flags flags) { flags_ = flags; } + void set_pin(gpio_num_t pin) { this->pin_ = static_cast(pin); } + void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; } + void set_drive_strength(gpio_drive_cap_t drive_strength) { + this->pin_flags_.drive_strength = static_cast(drive_strength); + } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } void setup() override; void pin_mode(gpio::Flags flags) override; @@ -21,17 +27,26 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { std::string dump_summary() const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return (uint8_t) pin_; } - gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + uint8_t get_pin() const override { return this->pin_; } + gpio::Flags get_flags() const override { return this->flags_; } + bool is_inverted() const override { return this->pin_flags_.inverted; } + gpio_num_t get_pin_num() const { return static_cast(this->pin_); } + gpio_drive_cap_t get_drive_strength() const { return static_cast(this->pin_flags_.drive_strength); } protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - gpio_num_t pin_; - gpio_drive_cap_t drive_strength_; - gpio::Flags flags_; - bool inverted_; + // Memory layout: 8 bytes total on 32-bit systems + // - 3 bytes for members below + // - 1 byte padding for alignment + // - 4 bytes for vtable pointer + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_; // GPIO flags (1 byte) + struct PinFlags { + uint8_t inverted : 1; // Invert pin logic (1 bit) + uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) + uint8_t reserved : 5; // Reserved for future use (5 bits) + } pin_flags_; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 2d3959b031..200ca567c2 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -58,8 +58,8 @@ extern "C" void resetPins() { // NOLINT #ifdef USE_ESP8266_EARLY_PIN_INIT for (int i = 0; i < 16; i++) { - uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; - uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; + uint8_t mode = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]); + uint8_t level = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]); if (mode != 255) pinMode(i, mode); // NOLINT if (level != 255) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2bc2291117..e7492fc505 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -199,11 +199,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] = {{{initial_modes_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] = {{{initial_levels_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index bb7e436bea..a26e9cc498 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include +#include extern "C" { #include "spi_flash.h" } @@ -119,16 +120,16 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: - size_t offset = 0; uint32_t type = 0; + uint16_t offset = 0; + uint8_t length_words = 0; // Max 255 words (1020 bytes of data) bool in_flash = false; - size_t length_words = 0; bool save(const uint8_t *data, size_t len) override { if (bytes_to_words(len) != length_words) { return false; } - size_t buffer_size = length_words + 1; + size_t buffer_size = static_cast(length_words) + 1; std::unique_ptr buffer(new uint32_t[buffer_size]()); // Note the () for zero-initialization memcpy(buffer.get(), data, len); buffer[length_words] = calculate_crc(buffer.get(), buffer.get() + length_words, type); @@ -142,7 +143,7 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { if (bytes_to_words(len) != length_words) { return false; } - size_t buffer_size = length_words + 1; + size_t buffer_size = static_cast(length_words) + 1; std::unique_ptr buffer(new uint32_t[buffer_size]()); bool ret = in_flash ? load_from_flash(offset, buffer.get(), buffer_size) : load_from_rtc(offset, buffer.get(), buffer_size); @@ -176,15 +177,19 @@ class ESP8266Preferences : public ESPPreferences { ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { uint32_t length_words = bytes_to_words(length); + if (length_words > 255) { + ESP_LOGE(TAG, "Preference too large: %" PRIu32 " words > 255", length_words); + return {}; + } if (in_flash) { uint32_t start = current_flash_offset; uint32_t end = start + length_words + 1; if (end > ESP8266_FLASH_STORAGE_SIZE) return {}; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = start; + pref->offset = static_cast(start); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = true; current_flash_offset = end; return {pref}; @@ -210,9 +215,9 @@ class ESP8266Preferences : public ESPPreferences { uint32_t rtc_offset = in_normal ? start + 32 : start - 96; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = rtc_offset; + pref->offset = static_cast(rtc_offset); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = false; current_offset += length_words + 1; return pref; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fc10e5366e..6654ef8748 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -30,19 +30,19 @@ void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_("creation"); + this->log_socket_error_(LOG_STR("creation")); this->mark_failed(); return; } int enable = 1; int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->log_socket_error_("reuseaddr"); + this->log_socket_error_(LOG_STR("reuseaddr")); // we can still continue } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_("non-blocking"); + this->log_socket_error_(LOG_STR("non-blocking")); this->mark_failed(); return; } @@ -51,21 +51,21 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_("set sockaddr"); + this->log_socket_error_(LOG_STR("set sockaddr")); this->mark_failed(); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_("bind"); + this->log_socket_error_(LOG_STR("bind")); this->mark_failed(); return; } err = this->server_->listen(4); if (err != 0) { - this->log_socket_error_("listen"); + this->log_socket_error_(LOG_STR("listen")); this->mark_failed(); return; } @@ -114,17 +114,17 @@ void ESPHomeOTAComponent::handle_handshake_() { return; int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { - this->log_socket_error_("nodelay"); + this->log_socket_error_(LOG_STR("nodelay")); this->cleanup_connection_(); return; } err = this->client_->setblocking(false); if (err != 0) { - this->log_socket_error_("non-blocking"); + this->log_socket_error_(LOG_STR("non-blocking")); this->cleanup_connection_(); return; } - this->log_start_("handshake"); + this->log_start_(LOG_STR("handshake")); this->client_connect_time_ = App.get_loop_component_start_time(); this->magic_buf_pos_ = 0; // Reset magic buffer position } @@ -150,7 +150,7 @@ void ESPHomeOTAComponent::handle_handshake_() { if (read <= 0) { // Error or connection closed if (read == -1) { - this->log_socket_error_("reading magic bytes"); + this->log_socket_error_(LOG_STR("reading magic bytes")); } else { ESP_LOGW(TAG, "Remote closed during handshake"); } @@ -209,7 +209,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read features - 1 byte if (!this->readall_(buf, 1)) { - this->log_read_error_("features"); + this->log_read_error_(LOG_STR("features")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT @@ -288,7 +288,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { - this->log_read_error_("size"); + this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_size = 0; @@ -302,7 +302,7 @@ void ESPHomeOTAComponent::handle_data_() { // starting the update, set the warning status and notify // listeners. This ensures that port scanners do not // accidentally trigger the update process. - this->log_start_("update"); + this->log_start_(LOG_STR("update")); this->status_set_warning(); #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); @@ -320,7 +320,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { - this->log_read_error_("MD5 checksum"); + this->log_read_error_(LOG_STR("MD5 checksum")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; @@ -393,7 +393,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { - this->log_read_error_("ack"); + this->log_read_error_(LOG_STR("ack")); // do not go to error, this is not fatal } @@ -477,12 +477,14 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } -void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); } +void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); +} -void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); } +void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } -void ESPHomeOTAComponent::log_start_(const char *phase) { - ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str()); +void ESPHomeOTAComponent::log_start_(const LogString *phase) { + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); } void ESPHomeOTAComponent::cleanup_connection_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c1919c71e9..f5a3e43ae3 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,10 +2,11 @@ #include "esphome/core/defines.h" #ifdef USE_OTA -#include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/socket/socket.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/preferences.h" namespace esphome { @@ -31,9 +32,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void handle_data_(); bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); - void log_socket_error_(const char *msg); - void log_read_error_(const char *what); - void log_start_(const char *phase); + void log_socket_error_(const LogString *msg); + void log_read_error_(const LogString *what); + void log_start_(const LogString *phase); void cleanup_connection_(); void yield_and_feed_watchdog_(); diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 87913488da..844a30bd8b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -492,7 +492,7 @@ void EthernetComponent::start_connect_() { global_eth_component->ipv6_count_ = 0; #endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); - this->status_set_warning("waiting for IP configuration"); + this->status_set_warning(LOG_STR("waiting for IP configuration")); esp_err_t err; err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 096b06917a..4c156ab24b 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -11,22 +11,22 @@ static const uint8_t NUMBER_OF_READ_RETRIES = 5; void GDK101Component::update() { uint8_t data[2]; if (!this->read_dose_1m_(data)) { - this->status_set_warning("Failed to read dose 1m"); + this->status_set_warning(LOG_STR("Failed to read dose 1m")); return; } if (!this->read_dose_10m_(data)) { - this->status_set_warning("Failed to read dose 10m"); + this->status_set_warning(LOG_STR("Failed to read dose 10m")); return; } if (!this->read_status_(data)) { - this->status_set_warning("Failed to read status"); + this->status_set_warning(LOG_STR("Failed to read status")); return; } if (!this->read_measurement_duration_(data)) { - this->status_set_warning("Failed to read measurement duration"); + this->status_set_warning(LOG_STR("Failed to read measurement duration")); return; } this->status_clear_warning(); diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 4b8369cd59..45544c185b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,6 +6,23 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +static const LogString *interrupt_type_to_string(gpio::InterruptType type) { + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + return LOG_STR("RISING_EDGE"); + case gpio::INTERRUPT_FALLING_EDGE: + return LOG_STR("FALLING_EDGE"); + case gpio::INTERRUPT_ANY_EDGE: + return LOG_STR("ANY_EDGE"); + default: + return LOG_STR("UNKNOWN"); + } +} + +static const LogString *gpio_mode_to_string(bool use_interrupt) { + return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling"); +} + void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); if (new_state != arg->last_state_) { @@ -51,25 +68,9 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; - ESP_LOGCONFIG(TAG, " Mode: %s", mode); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); if (this->use_interrupt_) { - const char *interrupt_type; - switch (this->interrupt_type_) { - case gpio::INTERRUPT_RISING_EDGE: - interrupt_type = "RISING_EDGE"; - break; - case gpio::INTERRUPT_FALLING_EDGE: - interrupt_type = "FALLING_EDGE"; - break; - case gpio::INTERRUPT_ANY_EDGE: - interrupt_type = "ANY_EDGE"; - break; - default: - interrupt_type = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); } } diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index d7230eb0b3..eeff98cb6e 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "esphome/core/hal.h" namespace esphome::gpio_expander { @@ -11,18 +12,27 @@ namespace esphome::gpio_expander { /// @brief A class to cache the read state of a GPIO expander. /// This class caches reads between GPIO Pins which are on the same bank. /// This means that for reading whole Port (ex. 8 pins) component needs only one -/// I2C/SPI read per main loop call. It assumes, that one bit in byte identifies one GPIO pin +/// I2C/SPI read per main loop call. It assumes that one bit in byte identifies one GPIO pin. +/// /// Template parameters: -/// T - Type which represents internal register. Could be uint8_t or uint16_t. Adjust to -/// match size of your internal GPIO bank register. -/// N - Number of pins -template class CachedGpioExpander { +/// T - Type which represents internal bank register. Could be uint8_t or uint16_t. +/// Choose based on how your I/O expander reads pins: +/// * uint8_t: For chips that read banks separately (8 pins at a time) +/// Examples: MCP23017 (2x8-bit banks), TCA9555 (2x8-bit banks) +/// * uint16_t: For chips that read all pins at once (up to 16 pins) +/// Examples: PCF8574/8575 (8/16 pins), PCA9554/9555 (8/16 pins) +/// N - Total number of pins (maximum 65535) +/// P - Type for pin number parameters (automatically selected based on N: +/// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified +/// if needed (e.g., for components like SN74HC165 with >256 pins) +template 256), uint16_t, uint8_t>::type> +class CachedGpioExpander { public: /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. /// @param pin Pin number to read /// @return Pin state - bool digital_read(T pin) { - const uint8_t bank = pin / BANK_SIZE; + bool digital_read(P pin) { + const P bank = pin / BANK_SIZE; const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { @@ -38,21 +48,31 @@ template class CachedGpioExpander { return this->digital_read_cache(pin); } - void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } + void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); } protected: - /// @brief Call component low level function to read GPIO state from device - virtual bool digital_read_hw(T pin) = 0; - /// @brief Call component read function from internal cache. - virtual bool digital_read_cache(T pin) = 0; - /// @brief Call component low level function to write GPIO state to device - virtual void digital_write_hw(T pin, bool value) = 0; + /// @brief Read GPIO bank from hardware into internal state + /// @param pin Pin number (used to determine which bank to read) + /// @return true if read succeeded, false on communication error + /// @note This does NOT return the pin state. It returns whether the read operation succeeded. + /// The actual pin state should be returned by digital_read_cache(). + virtual bool digital_read_hw(P pin) = 0; + + /// @brief Get cached pin value from internal state + /// @param pin Pin number to read + /// @return Pin state (true = HIGH, false = LOW) + virtual bool digital_read_cache(P pin) = 0; + + /// @brief Write GPIO state to hardware + /// @param pin Pin number to write + /// @param value Pin state to write (true = HIGH, false = LOW) + virtual void digital_write_hw(P pin, bool value) = 0; /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } - static constexpr uint8_t BITS_PER_BYTE = 8; - static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; + static constexpr uint16_t BITS_PER_BYTE = 8; + static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; static constexpr size_t BANKS = N / BANK_SIZE; static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index 52ec8433a2..b0f3429314 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -57,7 +57,7 @@ void GroveGasMultichannelV2Component::update() { void GroveGasMultichannelV2Component::dump_config() { ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_); LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_); LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_); diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 07218843dd..4810867d4b 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -20,7 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \ + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); \ return; \ } diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f293185cce..73696bd2a5 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -42,7 +42,7 @@ void HLW8012Component::dump_config() { " Current resistor: %.1f mΩ\n" " Voltage Divider: %.1f", this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp index 11f5dbc314..f173a1afbd 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "honeywellabp2"; void HONEYWELLABP2Sensor::read_sensor_data() { if (this->read(raw_data_, 7) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't read sensor data"); + this->status_set_warning(LOG_STR("couldn't read sensor data")); return; } float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]); // calculate digital pressure counts @@ -31,7 +31,7 @@ void HONEYWELLABP2Sensor::read_sensor_data() { void HONEYWELLABP2Sensor::start_measurement() { if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't start measurement"); + this->status_set_warning(LOG_STR("couldn't start measurement")); return; } this->measurement_running_ = true; @@ -40,7 +40,7 @@ void HONEYWELLABP2Sensor::start_measurement() { bool HONEYWELLABP2Sensor::is_measurement_ready() { if (this->read(raw_data_, 1) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't check measurement"); + this->status_set_warning(LOG_STR("couldn't check measurement")); return false; } if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) { @@ -53,7 +53,7 @@ bool HONEYWELLABP2Sensor::is_measurement_ready() { void HONEYWELLABP2Sensor::measurement_timeout() { ESP_LOGE(TAG, "Timeout!"); this->measurement_running_ = false; - this->status_set_warning("measurement timed out"); + this->status_set_warning(LOG_STR("measurement timed out")); } float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; } diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 48e1cf8aca..31c21f398c 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -39,18 +39,22 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t } ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { - std::vector v{}; - v.push_back(a_register); - v.insert(v.end(), data, data + len); - return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); + SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes + uint8_t *buffer = buffer_alloc.get(len + 1); + + buffer[0] = a_register; + std::copy(data, data + len, buffer + 1); + return this->bus_->write_readv(this->address_, buffer, len + 1, nullptr, 0); } ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { - std::vector v(len + 2); - v[0] = a_register >> 8; - v[1] = a_register; - std::copy(data, data + len, v.begin() + 2); - return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); + SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register + uint8_t *buffer = buffer_alloc.get(len + 2); + + buffer[0] = a_register >> 8; + buffer[1] = a_register; + std::copy(data, data + len, buffer + 2); + return this->bus_->write_readv(this->address_, buffer, len + 2, nullptr, 0); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index df4df628e8..1acbe506a3 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -10,6 +11,22 @@ namespace esphome { namespace i2c { +/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large +template class SmallBufferWithHeapFallback { + public: + uint8_t *get(size_t size) { + if (size <= STACK_SIZE) { + return this->stack_buffer_; + } + this->heap_buffer_ = std::unique_ptr(new uint8_t[size]); + return this->heap_buffer_.get(); + } + + private: + uint8_t stack_buffer_[STACK_SIZE]; + std::unique_ptr heap_buffer_; +}; + /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { NO_ERROR = 0, ///< No error found during execution of method @@ -74,14 +91,17 @@ class I2CBus { for (size_t i = 0; i != count; i++) { total_len += read_buffers[i].len; } - std::vector buffer(total_len); - auto err = this->write_readv(address, nullptr, 0, buffer.data(), total_len); + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small + uint8_t *buffer = buffer_alloc.get(total_len); + + auto err = this->write_readv(address, nullptr, 0, buffer, total_len); if (err != ERROR_OK) return err; size_t pos = 0; for (size_t i = 0; i != count; i++) { if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer.data() + pos, read_buffers[i].len); + std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); pos += read_buffers[i].len; } } @@ -91,11 +111,21 @@ class I2CBus { ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", "2025.9.0") ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - std::vector buffer{}; + size_t total_len = 0; for (size_t i = 0; i != count; i++) { - buffer.insert(buffer.end(), write_buffers[i].data, write_buffers[i].data + write_buffers[i].len); + total_len += write_buffers[i].len; } - return this->write_readv(address, buffer.data(), buffer.size(), nullptr, 0); + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small + uint8_t *buffer = buffer_alloc.get(total_len); + + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); + pos += write_buffers[i].len; + } + + return this->write_readv(address, buffer, total_len, nullptr, 0); } protected: diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index bfa1b726f1..cff91a546f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -4,6 +4,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -62,12 +65,15 @@ I2S_ROLE_OPTIONS = { CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT } -# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h (SOC_I2S_NUM) I2S_PORTS = { VARIANT_ESP32: 2, VARIANT_ESP32S2: 1, VARIANT_ESP32S3: 2, VARIANT_ESP32C3: 1, + VARIANT_ESP32C5: 1, + VARIANT_ESP32C6: 1, + VARIANT_ESP32H2: 1, VARIANT_ESP32P4: 3, } diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index d20e07460b..36f6d74ba0 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -33,7 +33,7 @@ void KMeterISOComponent::setup() { } uint8_t read_buf[4] = {1}; - if (!this->read_register(KMETER_ERROR_STATUS_REG, read_buf, 1)) { + if (!this->read_bytes(KMETER_ERROR_STATUS_REG, read_buf, 1)) { ESP_LOGCONFIG(TAG, "Could not read from the device."); this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 60945531cf..cbe9ed0454 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -11,19 +11,21 @@ static const char *const TAG = "light"; // Helper functions to reduce code size for logging #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN -static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max); +static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); } -static void log_feature_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': %s not supported", name, feature); +static void log_feature_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); } -static void log_color_mode_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, feature); +static void log_color_mode_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, LOG_STR_ARG(feature)); } -static void log_invalid_parameter(const char *name, const char *message) { ESP_LOGW(TAG, "'%s': %s", name, message); } +static void log_invalid_parameter(const char *name, const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); +} #else #define log_validation_warning(name, param_name, val, min, max) #define log_feature_not_supported(name, feature) @@ -201,19 +203,19 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "brightness"); + log_feature_not_supported(name, LOG_STR("brightness")); this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "transitions"); + log_feature_not_supported(name, LOG_STR("transitions")); this->set_flag_(FLAG_HAS_TRANSITION, false); } // Color brightness exists check if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB brightness"); + log_color_mode_not_supported(name, LOG_STR("RGB brightness")); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } @@ -221,7 +223,7 @@ LightColorValues LightCall::validate_() { if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB color"); + log_color_mode_not_supported(name, LOG_STR("RGB color")); this->set_flag_(FLAG_HAS_RED, false); this->set_flag_(FLAG_HAS_GREEN, false); this->set_flag_(FLAG_HAS_BLUE, false); @@ -231,21 +233,21 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->has_white() && this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "white value"); + log_color_mode_not_supported(name, LOG_STR("white value")); this->set_flag_(FLAG_HAS_WHITE, false); } // Color temperature exists check if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "color temperature"); + log_color_mode_not_supported(name, LOG_STR("color temperature")); this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); } // Cold/warm white value exists check if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "cold/warm white value"); + log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); this->set_flag_(FLAG_HAS_COLD_WHITE, false); this->set_flag_(FLAG_HAS_WARM_WHITE, false); } @@ -255,7 +257,7 @@ LightColorValues LightCall::validate_() { if (this->has_##name_()) { \ auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ - log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \ + log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \ this->name_##_ = clamp(val, (min), (max)); \ } \ } @@ -319,7 +321,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - log_invalid_parameter(name, "flash length must be greater than zero"); + log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); this->set_flag_(FLAG_HAS_FLASH, false); } @@ -338,13 +340,13 @@ LightColorValues LightCall::validate_() { } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - log_invalid_parameter(name, "effect cannot be used with transition/flash"); + log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); this->set_flag_(FLAG_HAS_TRANSITION, false); this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { - log_invalid_parameter(name, "flash cannot be used with transition"); + log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -361,7 +363,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - log_feature_not_supported(name, "transitions"); + log_feature_not_supported(name, LOG_STR("transitions")); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -371,7 +373,7 @@ LightColorValues LightCall::validate_() { bool target_state = this->has_state() ? this->state_ : v.is_on(); if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { - log_invalid_parameter(name, "cannot start effect when turning off"); + log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); this->set_flag_(FLAG_HAS_EFFECT, false); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 48323dd3c3..1427c02c35 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -12,6 +12,7 @@ #include "light_transformer.h" #include +#include namespace esphome { namespace light { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 195e04948d..5f0e78fc0d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -246,19 +246,40 @@ void Logger::add_on_log_callback(std::functionlog_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } + +#ifdef USE_STORE_LOG_STR_IN_FLASH +// ESP8266: PSTR() cannot be used in array initializers, so we need to declare +// each string separately as a global constant first +static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; +static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; +static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; +static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; +static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; +static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; +static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; +static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; + +static const LogString *const LOG_LEVELS[] = { + reinterpret_cast(LOG_LEVEL_NONE), reinterpret_cast(LOG_LEVEL_ERROR), + reinterpret_cast(LOG_LEVEL_WARN), reinterpret_cast(LOG_LEVEL_INFO), + reinterpret_cast(LOG_LEVEL_CONFIG), reinterpret_cast(LOG_LEVEL_DEBUG), + reinterpret_cast(LOG_LEVEL_VERBOSE), reinterpret_cast(LOG_LEVEL_VERY_VERBOSE), +}; +#else static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +#endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_LEVELS[ESPHOME_LOG_LEVEL], LOG_LEVELS[this->current_level_]); + LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" " Hardware UART: %s", - this->baud_rate_, get_uart_selection_()); + this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER if (this->log_buffer_) { @@ -267,14 +288,14 @@ void Logger::dump_config() { #endif for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); } } void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; this->level_callback_.call(level); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index aa76a188c9..a4cf5e3004 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -226,7 +226,7 @@ class Logger : public Component { } #ifndef USE_HOST - const char *get_uart_selection_(); + const LogString *get_uart_selection_(); #endif // Group 4-byte aligned members first diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 44243d4aa8..6cb57c1540 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -190,20 +190,28 @@ void HOT Logger::write_msg_(const char *msg) { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } #endif -const char *const UART_SELECTIONS[] = { - "UART0", "UART1", +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); #ifdef USE_ESP32_VARIANT_ESP32 - "UART2", + case UART_SELECTION_UART2: + return LOG_STR("UART2"); #endif #ifdef USE_LOGGER_USB_CDC - "USB_CDC", + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); #endif #ifdef USE_LOGGER_USB_SERIAL_JTAG - "USB_SERIAL_JTAG", + case UART_SELECTION_USB_SERIAL_JTAG: + return LOG_STR("USB_SERIAL_JTAG"); #endif -}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index fb5f6cee5d..5063d88b92 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -35,9 +35,17 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART0_SWAP: + default: + return LOG_STR("UART0_SWAP"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 09d0622bc3..3edfa74480 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -51,9 +51,19 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_DEFAULT: + return LOG_STR("DEFAULT"); + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART2: + default: + return LOG_STR("UART2"); + } +} } // namespace esphome::logger diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f1cad9b283..63727c2cda 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -29,9 +29,20 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif // USE_RP2040 diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 58a09facd5..817ca168f8 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -54,7 +54,7 @@ void Logger::pre_setup() { #endif } if (!device_is_ready(uart_dev)) { - ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); + ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; } @@ -77,9 +77,20 @@ void HOT Logger::write_msg_(const char *msg) { uart_poll_out(this->uart_dev_, '\n'); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp index 2f68d9f254..3eeba4a644 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp @@ -6,7 +6,7 @@ namespace m5stack_8angle { void M5Stack8AngleSwitchBinarySensor::update() { int8_t out = this->parent_->read_switch(); if (out == -1) { - this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read binary sensor state from M5Stack 8Angle.")); return; } this->publish_state(out != 0); diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp index 5e034f1dd3..d22b345141 100644 --- a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp @@ -7,7 +7,7 @@ void M5Stack8AngleKnobSensor::update() { if (this->parent_ != nullptr) { int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_); if (raw_pos == -1) { - this->status_set_warning("Could not read knob position from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read knob position from M5Stack 8Angle.")); return; } if (this->raw_) { diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index 8f486de6b7..f605fb1324 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -22,7 +22,7 @@ void MAX17043Component::update() { if (this->voltage_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { - this->status_set_warning("Unable to read MAX17043_VCELL"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL")); } else { float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; this->voltage_sensor_->publish_state(voltage); @@ -31,7 +31,7 @@ void MAX17043Component::update() { } if (this->battery_remaining_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { - this->status_set_warning("Unable to read MAX17043_SOC"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC")); } else { float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); this->battery_remaining_sensor_->publish_state(percent); diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 3333e46c97..5a1f011617 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 9d8d6e4dae..be86cb2256 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -22,14 +22,29 @@ void MCP23016::setup() { this->write_reg_(MCP23016_IODIR0, 0xFF); this->write_reg_(MCP23016_IODIR1, 0xFF); } -bool MCP23016::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; + +void MCP23016::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} +bool MCP23016::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; uint8_t value = 0; - this->read_reg_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_reg_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void MCP23016::digital_write(uint8_t pin, bool value) { + +bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } +void MCP23016::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; this->update_reg_(pin, value, reg_addr); } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index e4ed47a3b2..781c207de0 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace mcp23016 { @@ -24,19 +25,22 @@ enum MCP23016GPIORegisters { MCP23016_IOCON1 = 0x0B, }; -class MCP23016 : public Component, public i2c::I2CDevice { +class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { public: MCP23016() = default; void setup() override; - - bool digital_read(uint8_t pin); - void digital_write(uint8_t pin, bool value); + void loop() override; void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + // read a given register bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register @@ -46,6 +50,8 @@ class MCP23016 : public Component, public i2c::I2CDevice { uint8_t olat_0_{0x00}; uint8_t olat_1_{0x00}; + // Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; }; class MCP23016GPIOPin : public GPIOPin { diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index e4fb51174b..1593c376cd 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "mcp23x08_base"; bool MCP23X08Base::digital_read_hw(uint8_t pin) { if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } return true; diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 020b8a5ddf..b1f1f260b4 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -11,13 +11,13 @@ bool MCP23X17Base::digital_read_hw(uint8_t pin) { uint8_t data; if (pin < 8) { if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data); } else { if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF); diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 316a10596f..5d9788198f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -5,6 +5,30 @@ #include "esphome/core/version.h" #include "mdns_component.h" +#ifdef USE_ESP8266 +#include +// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms +#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value +// Helper to get string from PROGMEM - returns a temporary std::string +// Only define this function if we have services that will use it +#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES) +static std::string mdns_string_p(const char *src) { + char buf[64]; + strncpy_P(buf, src, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return std::string(buf); +} +#define MDNS_STR(name) mdns_string_p(name) +#else +// If no services are configured, we still need the fallback service but it uses string literals +#define MDNS_STR(name) std::string(name) +#endif +#else +// On non-ESP8266 platforms, use regular const char* +#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value +#define MDNS_STR(name) name +#endif + #ifdef USE_API #include "esphome/components/api/api_server.h" #endif @@ -21,6 +45,32 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +// Define all constant strings using the macro +MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); +MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); +MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http"); +MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + +MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); +MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); +MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); +MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); +MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); +MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); +MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption"); +MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported"); +MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name"); +MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version"); +MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url"); + +MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); +MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32"); +MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040"); + +MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); +MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); +MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); + void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); @@ -50,8 +100,8 @@ void MDNSComponent::compile_records_() { if (api::global_api_server != nullptr) { this->services_.emplace_back(); auto &service = this->services_.back(); - service.service_type = "_esphomelib"; - service.proto = "_tcp"; + service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); + service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); const std::string &friendly_name = App.get_friendly_name(); @@ -82,47 +132,47 @@ void MDNSComponent::compile_records_() { txt_records.reserve(txt_count); if (!friendly_name_empty) { - txt_records.emplace_back(MDNSTXTRecord{"friendly_name", friendly_name}); + txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name}); } - txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); - txt_records.emplace_back(MDNSTXTRecord{"mac", get_mac_address()}); + txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); + txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); #ifdef USE_ESP8266 - txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP8266"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); #elif defined(USE_ESP32) - txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP32"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); #elif defined(USE_RP2040) - txt_records.emplace_back(MDNSTXTRecord{"platform", "RP2040"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); #elif defined(USE_LIBRETINY) txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()}); #endif - txt_records.emplace_back(MDNSTXTRecord{"board", ESPHOME_BOARD}); + txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); #if defined(USE_WIFI) - txt_records.emplace_back(MDNSTXTRecord{"network", "wifi"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); #elif defined(USE_ETHERNET) - txt_records.emplace_back(MDNSTXTRecord{"network", "ethernet"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); #elif defined(USE_OPENTHREAD) - txt_records.emplace_back(MDNSTXTRecord{"network", "thread"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); #endif #ifdef USE_API_NOISE - static constexpr const char *NOISE_ENCRYPTION = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); if (api::global_api_server->get_noise_ctx()->has_psk()) { - txt_records.emplace_back(MDNSTXTRecord{"api_encryption", NOISE_ENCRYPTION}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); } else { - txt_records.emplace_back(MDNSTXTRecord{"api_encryption_supported", NOISE_ENCRYPTION}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)}); } #endif #ifdef ESPHOME_PROJECT_NAME - txt_records.emplace_back(MDNSTXTRecord{"project_name", ESPHOME_PROJECT_NAME}); - txt_records.emplace_back(MDNSTXTRecord{"project_version", ESPHOME_PROJECT_VERSION}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - txt_records.emplace_back(MDNSTXTRecord{"package_import_url", dashboard_import::get_package_import_url()}); + txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()}); #endif } #endif // USE_API @@ -130,16 +180,16 @@ void MDNSComponent::compile_records_() { #ifdef USE_PROMETHEUS this->services_.emplace_back(); auto &prom_service = this->services_.back(); - prom_service.service_type = "_prometheus-http"; - prom_service.proto = "_tcp"; + prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); + prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER this->services_.emplace_back(); auto &web_service = this->services_.back(); - web_service.service_type = "_http"; - web_service.proto = "_tcp"; + web_service.service_type = MDNS_STR(SERVICE_HTTP); + web_service.proto = MDNS_STR(SERVICE_TCP); web_service.port = USE_WEBSERVER_PORT; #endif diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index a9ecb9d79a..8b1ca899df 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -222,6 +222,12 @@ def delay(ms): class DriverChip: + """ + A class representing a MIPI DBI driver chip model. + The parameters supplied as defaults will be used to provide default values for the display configuration. + Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes. + """ + models: dict[str, Self] = {} def __init__( @@ -232,7 +238,7 @@ class DriverChip: ): name = name.upper() self.name = name - self.initsequence = initsequence or defaults.get("init_sequence") + self.initsequence = initsequence self.defaults = defaults DriverChip.models[name] = self @@ -246,6 +252,17 @@ class DriverChip: return models def extend(self, name, **kwargs) -> "DriverChip": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence)) + initsequence.extend(kwargs.pop("add_init_sequence", ())) defaults = self.defaults.copy() if ( CONF_WIDTH in defaults @@ -260,23 +277,39 @@ class DriverChip: ): defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] defaults.update(kwargs) - return DriverChip(name, initsequence=self.initsequence, **defaults) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) def get_default(self, key, fallback: Any = False) -> Any: return self.defaults.get(key, fallback) + @property + def transforms(self) -> set[str]: + """ + Return the available transforms for this model. + """ + if self.get_default("no_transform", False): + return set() + if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED: + return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + def option(self, name, fallback=False) -> cv.Optional: return cv.Optional(name, default=self.get_default(name, fallback)) def rotation_as_transform(self, config) -> bool: """ Check if a rotation can be implemented in hardware using the MADCTL register. - A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y. """ + transforms = self.transforms rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) + if rotation == 0 or not transforms: + return False + if rotation == 180: + return CONF_MIRROR_X in transforms and CONF_MIRROR_Y in transforms + if rotation == 90: + return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms + return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms def get_dimensions(self, config) -> tuple[int, int, int, int]: if CONF_DIMENSIONS in config: @@ -301,10 +334,10 @@ class DriverChip: # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where # the offset is asymmetric - if transform[CONF_MIRROR_X]: + if transform.get(CONF_MIRROR_X): native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: + if transform.get(CONF_MIRROR_Y): native_height = self.get_default( CONF_NATIVE_HEIGHT, height + offset_height * 2 ) @@ -314,7 +347,7 @@ class DriverChip: 90, 270, ) - if transform[CONF_SWAP_XY] is True or rotated: + if transform.get(CONF_SWAP_XY) is True or rotated: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height @@ -324,27 +357,50 @@ class DriverChip: transform = config.get( CONF_TRANSFORM, { - CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) + # fill in defaults if not provided + mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X)) + mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y)) + swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY)) + transform[CONF_MIRROR_X] = mirror_x + transform[CONF_MIRROR_Y] = mirror_y + transform[CONF_SWAP_XY] = swap_xy # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_MIRROR_X] = not mirror_x + transform[CONF_MIRROR_Y] = not mirror_y elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_X] = not mirror_x else: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_Y] = not mirror_y transform[CONF_TRANSFORM] = True return transform + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if transform[CONF_MIRROR_X]: + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform[CONF_MIRROR_Y]: + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + return madctl + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. @@ -367,21 +423,9 @@ class DriverChip: pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) - # Does the chip use the flipping bits for mirroring rather than the reverse order bits? - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) + madctl = self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: diff --git a/esphome/components/mipi_rgb/__init__.py b/esphome/components/mipi_rgb/__init__.py new file mode 100644 index 0000000000..4f9972c6e0 --- /dev/null +++ b/esphome/components/mipi_rgb/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DOMAIN = "mipi_rgb" diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py new file mode 100644 index 0000000000..3001d33980 --- /dev/null +++ b/esphome/components/mipi_rgb/display.py @@ -0,0 +1,321 @@ +import importlib +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_PIN, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_18BIT, + DriverChip, + dimension_schema, + map_sequence, + power_of_two, + requires_buffer, +) +from esphome.components.rpi_dpi_rgb.display import ( + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_NUMBER, + CONF_RED, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SPI_ID, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent +from . import models + +DEPENDENCIES = ["esp32", "psram"] + +mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb") +mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component) +mipi_rgb_spi = mipi_rgb_ns.class_( + "MipiRgbSpi", mipi_rgb, display.Display, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") + +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + if transforms := model.transforms: + transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) + for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): + if x not in transforms: + transform = transform.extend( + {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} + ) + else: + transform = cv.invalid("This model does not support transforms") + + # RPI model does not use an init sequence, indicates with empty list + if model.initsequence is None: + # Custom model requires an init sequence + iseqconf = cv.Required(CONF_INIT_SEQUENCE) + uses_spi = True + else: + iseqconf = cv.Optional(CONF_INIT_SEQUENCE) + uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 + swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) + + # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(mipi_rgb_spi if uses_spi else mipi_rgb), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_PCLK_INVERTED, True): cv.boolean, + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_BIG): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + model.option( + CONF_DE_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, + model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + if uses_spi: + schema = schema.extend( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + model.option(CONF_DATA_RATE, "1MHz"): SPI_DATA_RATE_SCHEMA, + model.option(CONF_SPI_MODE, "MODE0"): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + return schema + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + schema = model_schema(config) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, + )(config) + + +CONFIG_SCHEMA = _config_schema + + +def _final_validate(config): + global_config = full_config.get() + + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if not requires_buffer(config) and LVGL_DOMAIN not in global_config: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + if CONF_SPI_ID in config: + config = spi.final_validate_device_schema( + "mipi_rgb", require_miso=False, require_mosi=True + )(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height) + cg.add(var.set_model(model.name)) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if CONF_SPI_ID in config: + await spi.register_spi_device(var, config) + sequence, madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + index = 0 + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + red_pins = config[CONF_DATA_PINS][CONF_RED] + green_pins = config[CONF_DATA_PINS][CONF_GREEN] + blue_pins = config[CONF_DATA_PINS][CONF_BLUE] + if config[CONF_COLOR_ORDER] == "BGR": + dpins.extend(red_pins) + dpins.extend(green_pins) + dpins.extend(blue_pins) + else: + dpins.extend(blue_pins) + dpins.extend(green_pins) + dpins.extend(red_pins) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for index, pin in enumerate(dpins): + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + + if dc_pin := config.get(CONF_DC_PIN): + dc = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + + if de_pin := config.get(CONF_DE_PIN): + pin = await cg.gpio_pin_expression(de_pin) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) + + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp new file mode 100644 index 0000000000..00c9c8cbff --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -0,0 +1,388 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "mipi_rgb.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace mipi_rgb { + +static const uint8_t DELAY_FLAG = 0xFF; +static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_ML = 0x10; // Bit 4 Refresh bottom to top +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +void MipiRgb::setup_enables_() { + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } +} + +#ifdef USE_SPI +void MipiRgbSpi::setup() { + this->setup_enables_(); + this->spi_setup(); + this->write_init_sequence_(); + this->common_setup_(); +} +void MipiRgbSpi::write_command_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value, 9); + } else { + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + } + this->disable(); +} + +void MipiRgbSpi::write_data_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value | 0x100, 9); + } else { + this->dc_pin_->digital_write(true); + this->write_byte(value); + } + this->disable(); +} + +/** + * this relies upon the init sequence being well-formed, which is guaranteed by the Python init code. + */ + +void MipiRgbSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + this->mark_failed("Malformed init sequence"); + return; + } + if (cmd == SLEEP_OUT) { + delay(120); // NOLINT + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + index += num_args; + this->write_command_(cmd); + while (num_args-- != 0) + this->write_data_(*ptr++); + if (cmd == SLEEP_OUT) + delay(10); + } + } + // this->spi_teardown(); // SPI not needed after this + this->init_sequence_.clear(); + delay(10); +} + +void MipiRgbSpi::dump_config() { + MipiRgb::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, + " SPI Data rate: %uMHz" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Color Order: %s", + (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), + this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); +} + +#endif // USE_SPI + +void MipiRgb::setup() { + this->setup_enables_(); + this->common_setup_(); +} + +void MipiRgb::common_setup_() { + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + if (this->de_pin_) { + config.de_gpio_num = this->de_pin_->get_pin(); + } else { + config.de_gpio_num = -1; + } + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_reset(this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(msg.c_str()); + } + ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); +} + +void MipiRgb::loop() { + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +} + +void MipiRgb::update() { + if (this->is_failed()) + return; + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, reinterpret_cast(this->buffer_), + this->x_low_, this->y_low_, this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MipiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0 || this->is_failed()) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + this->write_to_display_(x_start, y_start, w, h, reinterpret_cast(this->buffer_), x_start, y_start, + this->width_ - w - x_start); + } else { + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); + } +} + +void MipiRgb::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + esp_err_t err = ESP_OK; + auto stride = (x_offset + w + x_pad) * 2; + ptr += y_offset * stride + x_offset * 2; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MipiRgb::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_); + if (this->buffer_ == nullptr) { + this->mark_failed("Could not allocate buffer for display!"); + return false; + } + return true; +} + +void MipiRgb::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y) || this->is_failed()) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + if (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} +void MipiRgb::fill(Color color) { + if (!this->check_buffer_()) + return; + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); +} + +int MipiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MipiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static std::string get_pin_name(GPIOPin *pin) { + if (pin == nullptr) + return "None"; + return pin->dump_summary(); +} + +void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) { + for (uint8_t i = start; i != end; i++) { + ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str()); + } +} + +void MipiRgb::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Rotation: %d degrees" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Invert Colors: %s" + "\n Pixel Clock: %dMHz" + "\n Reset Pin: %s" + "\n DE Pin: %s" + "\n PCLK Pin: %s" + "\n HSYNC Pin: %s" + "\n VSYNC Pin: %s", + this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, + this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, + this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, + get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), + get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), + get_pin_name(this->vsync_pin_).c_str()); + + if (this->madctl_ & MADCTL_BGR) { + this->dump_pins_(8, 13, "Blue", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Red", 0); + } else { + this->dump_pins_(8, 13, "Red", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Blue", 0); + } +} + +} // namespace mipi_rgb +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h new file mode 100644 index 0000000000..173e23752d --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -0,0 +1,127 @@ +#pragma once + +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/gpio.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#ifdef USE_SPI +#include "esphome/components/spi/spi.h" +#endif + +namespace esphome { +namespace mipi_rgb { + +constexpr static const char *const TAG = "display.mipi_rgb"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; + +class MipiRgb : public display::Display { + public: + MipiRgb(int width, int height) : width_(width), height_(height) {} + void setup() override; + void loop() override; + void update() override; + void fill(Color color); + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + bool check_buffer_(); + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_model(const char *model) { this->model_ = model; } + int get_width() override; + int get_height() override; + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset); + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + // this will be horribly slow. + protected: + void setup_enables_(); + void common_setup_(); + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + uint8_t madctl_{}; + const char *model_{"Unknown"}; + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_; + size_t height_; + uint16_t *buffer_{nullptr}; + std::vector enable_pins_{}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + esp_lcd_panel_handle_t handle_{}; +}; + +#ifdef USE_SPI +class MipiRgbSpi : public MipiRgb, + public spi::SPIDevice { + public: + MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} + + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void setup() override; + + protected: + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_init_sequence_(); + void dump_config(); + + GPIOPin *dc_pin_{nullptr}; + std::vector init_sequence_; +}; +#endif + +} // namespace mipi_rgb +} // namespace esphome +#endif diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py new file mode 100644 index 0000000000..da433e686e --- /dev/null +++ b/esphome/components/mipi_rgb/models/guition.py @@ -0,0 +1,24 @@ +from .st7701s import st7701s + +st7701s.extend( + "GUITION-4848S040", + width=480, + height=480, + data_rate="2MHz", + cs_pin=39, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_frequency="12MHz", + pixel_mode="18bit", + mirror_x=True, + mirror_y=True, + data_pins={ + "red": [11, 12, 13, 14, 0], + "green": [8, 20, 3, 46, 9, 10], + "blue": [4, 5, 6, 7, 15], + }, + # Additional configuration for Guition 4848S040, 16 bit bus config + add_init_sequence=((0xCD, 0x00),), +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index e69de29bb2..109dc42af6 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -0,0 +1,228 @@ +from esphome.config_validation import UNDEFINED + +from .st7701s import ST7701S + +# fmt: off +ST7701S( + "T-PANEL-S3", + width=480, + height=480, + color_order="BGR", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 17}, + reset_pin={"xl9535": None, "number": 5}, + hsync_pin=39, + vsync_pin=40, + pclk_pin=41, + data_pins={ + "red": [12, 13, 42, 46, 45], + "green": [6, 7, 8, 9, 10, 11], + "blue": [1, 2, 3, 4, 5], + }, + hsync_front_porch=20, + hsync_back_porch=0, + hsync_pulse_width=2, + vsync_front_porch=30, + vsync_back_porch=1, + vsync_pulse_width=8, + pclk_frequency="6MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x30, 0x02, 0x37), (0xCC, 0x10), + (0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x4D), (0xB1, 0x33), (0xB2, 0x87), (0xB5, 0x4B), (0xB7, 0x8C), (0xB8, 0x20), (0xC1, 0x78), + (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), (0xE4, 0x44, 0x44), + (0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0), + (0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40), + (0xEC, 0x78, 0x00), + (0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ), +) + + +t_rgb = ST7701S( + "T-RGB-2.1", + width=480, + height=480, + color_order="BGR", + pixel_mode="18bit", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 3}, + de_pin=45, + hsync_pin=47, + vsync_pin=41, + pclk_pin=42, + data_pins={ + "red": [7, 6, 5, 3, 2], + "green": [14, 13, 12, 11, 10, 9], + "blue": [21, 18, 17, 16, 15], + }, + hsync_front_porch=50, + hsync_pulse_width=1, + hsync_back_porch=30, + vsync_front_porch=20, + vsync_pulse_width=1, + vsync_back_porch=30, + pclk_frequency="12MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + + (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), + (0xC2, 0x07, 0x02), + (0xCC, 0x10), + (0xCD, 0x08), + + (0xB0, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x06, 0x05, 0x09, + 0x08, 0x21, 0x06, 0x13, + 0x10, 0x29, 0x31, 0x18), + + (0xB1, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x07, 0x05, 0x09, + 0x09, 0x21, 0x05, 0x13, + 0x11, 0x2a, 0x31, 0x18), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + + (0xB0, 0x6D), + (0xB1, 0x37), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x43), + (0xB7, 0x85), + (0xB8, 0x20), + + (0xC1, 0x78), + (0xC2, 0x78), + (0xC3, 0x8C), + + (0xD0, 0x88), + + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x03, 0xA0, 0x00, 0x00, + 0x04, 0xA0, 0x00, 0x00, + 0x00, 0x20, 0x20), + + (0xE2, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00), + + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + + (0xE5, + 0x05, 0xEC, 0xA0, 0xA0, + 0x07, 0xEE, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + + (0xE8, + 0x06, 0xED, 0xA0, 0xA0, + 0x08, 0xEF, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x10), + + (0xED, + 0xFF, 0xFF, 0xFF, 0xBA, + 0x0A, 0xBF, 0x45, 0xFF, + 0xFF, 0x54, 0xFB, 0xA0, + 0xAB, 0xFF, 0xFF, 0xFF), + + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10) + ) + +) +t_rgb.extend( + "T-RGB-2.8", + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), + (0xC1, 0x10, 0x0C), + (0xC2, 0x07, 0x0A), + (0xC7, 0x00), + (0xC7, 0x10), + (0xCD, 0x08), + (0xB0, + 0x05, 0x12, 0x98, 0x0e, 0x0F, + 0x07, 0x07, 0x09, 0x09, 0x23, + 0x05, 0x52, 0x0F, 0x67, 0x2C, 0x11), + (0xB1, + 0x0B, 0x11, 0x97, 0x0C, 0x12, + 0x06, 0x06, 0x08, 0x08, 0x22, + 0x03, 0x51, 0x11, 0x66, 0x2B, 0x0F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x2D), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x20), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x06, 0x30, 0x08, 0x30, 0x05, + 0x30, 0x07, 0x30, 0x00, 0x33, + 0x33), + (0xE2, + 0x11, 0x11, 0x33, 0x33, 0xf4, + 0x00, 0x00, 0x00, 0xf4, 0x00, + 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, + 0x0d, 0xf5, 0x30, 0xf0, 0x0f, + 0xf7, 0x30, 0xf0, 0x09, 0xf1, + 0x30, 0xf0, 0x0b, 0xf3, 0x30, 0xf0), + (0xE6, 0x00, 0x00, 0x11, 0x11), + (0xE7, 0x44, 0x44), + (0xE8, + 0x0c, 0xf4, 0x30, 0xf0, + 0x0e, 0xf6, 0x30, 0xf0, + 0x08, 0xf0, 0x30, 0xf0, + 0x0a, 0xf2, 0x30, 0xf0), + (0xe9, 0x36), + (0xEB, 0x00, 0x01, 0xe4, 0xe4, 0x44, 0x88, 0x40), + (0xED, + 0xff, 0x10, 0xaf, 0x76, + 0x54, 0x2b, 0xcf, 0xff, + 0xff, 0xfc, 0xb2, 0x45, + 0x67, 0xfa, 0x01, 0xff), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3f, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ) +) diff --git a/esphome/components/mipi_rgb/models/rpi.py b/esphome/components/mipi_rgb/models/rpi.py new file mode 100644 index 0000000000..076d96b658 --- /dev/null +++ b/esphome/components/mipi_rgb/models/rpi.py @@ -0,0 +1,9 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence +DriverChip( + "RPI", + swap_xy=UNDEFINED, + initsequence=(), +) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py new file mode 100644 index 0000000000..bfd1c9aa3f --- /dev/null +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -0,0 +1,214 @@ +from esphome.components.mipi import ( + MADCTL, + MADCTL_ML, + MADCTL_XFLIP, + MODE_BGR, + DriverChip, +) +from esphome.config_validation import UNDEFINED +from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y + +SDIR_CMD = 0xC7 + + +class ST7701S(DriverChip): + # The ST7701s does not use the standard MADCTL bits for x/y mirroring + def add_madctl(self, sequence: list, config: dict): + transform = self.get_transform(config) + madctl = 0x00 + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= 0x08 + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_ML + sequence.append((MADCTL, madctl)) + sdir = 0 + if transform.get(CONF_MIRROR_X): + sdir |= 0x04 + madctl |= MADCTL_XFLIP + sequence.append((SDIR_CMD, sdir)) + return madctl + + @property + def transforms(self) -> set[str]: + """ + The ST7701 never supports axis swapping, and mirroring the y-axis only works for full height. + """ + if self.get_default(CONF_HEIGHT) != 864: + return {CONF_MIRROR_X} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + + +# fmt: off +st7701s = ST7701S( + "ST7701S", + width=480, + height=864, + swap_xy=UNDEFINED, + hsync_front_porch=20, + hsync_back_porch=10, + hsync_pulse_width=10, + vsync_front_porch=10, + vsync_back_porch=10, + vsync_pulse_width=10, + pclk_frequency="16MHz", + pclk_inverted=True, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), + (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), + (0xB1, 0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), # page 1 + (0xB0, 0x60), (0xB1, 0x32), (0xB2, 0x07), (0xB3, 0x80), (0xB5, 0x49), (0xB7, 0x85), (0xB8, 0x21), (0xC1, 0x78), + (0xC2, 0x78), (0xE0, 0x00, 0x1B, 0x02), + (0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, 0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0,), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0,), + (0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40), (0xEC, 0x3C, 0x00), + (0xED, 0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), # Page 3 + (0xE5, 0xE4), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xCD, 0x08), + ) +) + +st7701s.extend( + "MAKERFABS-4", + width=480, + height=480, + color_order="RGB", + invert_colors=True, + pixel_mode="18bit", + cs_pin=1, + de_pin={ + "number": 45, + "ignore_strapping_warning": True + }, + hsync_pin=5, + vsync_pin=4, + pclk_pin=21, + data_pins={ + "red": [39, 40, 41, 42, 2], + "green": [0, 9, 14, 47, 48, 3], + "blue": [6, 7, 15, 16, 8] + } +) + +st7701s.extend( + "SEEED-INDICATOR-D1", + width=480, + height=480, + mirror_x=True, + mirror_y=True, + invert_colors=True, + pixel_mode="18bit", + spi_mode="MODE3", + data_rate="2MHz", + hsync_front_porch=10, + hsync_pulse_width=8, + hsync_back_porch=50, + vsync_front_porch=10, + vsync_pulse_width=8, + vsync_back_porch=20, + cs_pin={"pca9554": None, "number": 4}, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_inverted=False, + data_pins={ + "red": [4, 3, 2, 1, 0], + "green": [10, 9, 8, 7, 6, 5], + "blue": [15, 14, 13, 12, 11] + }, +) + +st7701s.extend( + "UEDX48480021-MD80ET", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=18, + reset_pin=8, + de_pin=17, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + pclk_pin=9, + data_pins={ + "red": [40, 41, 42, 2, 1], + "green": [21, 47, 48, 45, 38, 39], + "blue": [10, 11, {"number": 12, "allow_other_uses": True}, {"number": 13, "allow_other_uses": True}, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xC7, 0x00), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2A, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x6D), (0xB1, 0x37), (0xB2, 0x8B), (0xB3, 0x80), (0xB5, 0x43), (0xB7, 0x85), + (0xB8, 0x20), (0xC0, 0x09), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xF6, 0xCA, 0x07, 0xEE, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xF6, 0xCA, 0x08, 0xEF, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE9, 0x36, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xFF, 0x45, 0xFF, 0xFF, 0x54, 0xFF, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0E), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + (0x11, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0C), + (0xE8, 0x00, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) + +st7701s.extend( + "ZX2D10GE01R-V4848", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=21, + de_pin=39, + vsync_pin=48, + hsync_pin=40, + pclk_pin={"number": 45, "ignore_strapping_warning": True}, + pclk_frequency="15MHz", + pclk_inverted=True, + hsync_pulse_width=10, + hsync_back_porch=10, + hsync_front_porch=10, + vsync_pulse_width=2, + vsync_back_porch=12, + vsync_front_porch=14, + data_pins={ + "red": [10, 16, 9, 15, 46], + "green": [8, 13, 18, 12, 11, 17], + "blue": [{"number": 47, "allow_other_uses": True}, {"number": 41, "allow_other_uses": True}, 0, 42, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2a, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), (0xB0, 0x6d), (0xB1, 0x37), (0xB2, 0x81), (0xB3, 0x80), (0xB5, 0x43), + (0xB7, 0x85), (0xB8, 0x20), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xA0, 0xA0, 0x07, 0xEE, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xA0, 0xA0, 0x08, 0xEF, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xBF, 0x45, 0xFF, 0xFF, 0x54, 0xFB, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py new file mode 100644 index 0000000000..49a75da232 --- /dev/null +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -0,0 +1,64 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +from .st7701s import st7701s + +wave_4_3 = DriverChip( + "ESP32-S3-TOUCH-LCD-4.3", + swap_xy=UNDEFINED, + initsequence=(), + width=800, + height=480, + pclk_frequency="16MHz", + reset_pin={"ch422g": None, "number": 3}, + enable_pin={"ch422g": None, "number": 2}, + de_pin=5, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + pclk_pin=7, + pclk_inverted=True, + hsync_front_porch=210, + hsync_pulse_width=30, + hsync_back_porch=30, + vsync_front_porch=4, + vsync_pulse_width=4, + vsync_back_porch=4, + data_pins={ + "red": [1, 2, 42, 41, 40], + "green": [39, 0, 45, 48, 47, 21], + "blue": [14, 38, 18, 17, 10], + }, +) +wave_4_3.extend( + "ESP32-S3-TOUCH-LCD-7-800X480", + enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], + hsync_back_porch=8, + hsync_front_porch=8, + hsync_pulse_width=4, + vsync_back_porch=16, + vsync_front_porch=16, + vsync_pulse_width=4, +) + +st7701s.extend( + "WAVESHARE-4-480x480", + data_rate="2MHz", + spi_mode="MODE3", + color_order="BGR", + pixel_mode="18bit", + width=480, + height=480, + invert_colors=True, + cs_pin=42, + de_pin=40, + hsync_pin=38, + vsync_pin=39, + pclk_pin=41, + pclk_frequency="12MHz", + pclk_inverted=False, + data_pins={ + "red": [46, 3, 8, 18, 17], + "green": [14, 13, 12, 11, 10, 9], + "blue": [5, 45, 48, 47, 21], + }, +) diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 2e1db1908f..9e61f6ef3b 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -58,8 +58,13 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon if (this->sensor_->get_force_update()) root[MQTT_FORCE_UPDATE] = true; - if (this->sensor_->get_state_class() != STATE_CLASS_NONE) - root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); + if (this->sensor_->get_state_class() != STATE_CLASS_NONE) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + root[MQTT_STATE_CLASS] = (const __FlashStringHelper *) state_class_to_string(this->sensor_->get_state_class()); +#else + root[MQTT_STATE_CLASS] = LOG_STR_ARG(state_class_to_string(this->sensor_->get_state_class())); +#endif + } config.command_topic = false; } diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 8f8c05eb7d..5a7622e783 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,13 +19,14 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); // NOLINT - for (uint8_t offset = 0; offset < 6; offset++) { - if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { - this->mark_failed(); - return; + this->set_timeout(100, [this]() { + for (uint8_t offset = 0; offset < 6; offset++) { + if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { + this->mark_failed(); + return; + } } - } + }); } void MS5611Component::dump_config() { ESP_LOGCONFIG(TAG, "MS5611:"); diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index c47b393f99..7ddd7a2f08 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -11,7 +11,10 @@ static const char *const TAG = "nextion.upload"; bool Nextion::upload_end_(bool successful) { if (successful) { ESP_LOGD(TAG, "Upload successful"); - delay(1500); // NOLINT + for (uint8_t i = 0; i <= 5; i++) { + delay(1000); // NOLINT + App.feed_wdt(); // Feed the watchdog timer. + } App.safe_reboot(); } else { ESP_LOGE(TAG, "Upload failed"); diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index da6c4623c9..e540edb91f 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CODEOWNERS = ["@Mat931"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["gpio_expander"] MULTI_CONF = True pca6416a_ns = cg.esphome_ns.namespace("pca6416a") diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index 730c494e34..c0056e780b 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -51,6 +51,11 @@ void PCA6416AComponent::setup() { this->status_has_error()); } +void PCA6416AComponent::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} + void PCA6416AComponent::dump_config() { if (this->has_pullup_) { ESP_LOGCONFIG(TAG, "PCAL6416A:"); @@ -63,15 +68,25 @@ void PCA6416AComponent::dump_config() { } } -bool PCA6416AComponent::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; +bool PCA6416AComponent::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; uint8_t value = 0; - this->read_register_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_register_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void PCA6416AComponent::digital_write(uint8_t pin, bool value) { +bool PCA6416AComponent::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCA6416AComponent::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; this->update_register_(pin, value, reg_addr); } diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 1e8015c40a..10a4a64e9b 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -3,20 +3,20 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca6416a { -class PCA6416AComponent : public Component, public i2c::I2CDevice { +class PCA6416AComponent : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA6416AComponent() = default; /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -25,6 +25,11 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { void dump_config() override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_register_(uint8_t reg, uint8_t *value); bool write_register_(uint8_t reg, uint8_t value); void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); @@ -32,6 +37,8 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { /// The mask to write as output state - 1 means HIGH, 0 means LOW uint8_t output_0_{0x00}; uint8_t output_1_{0x00}; + /// Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; /// Only the PCAL6416A has pull-up resistors diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 05713cccda..626b08a378 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_OUTPUT, ) -CODEOWNERS = ["@hwstar", "@clydebarrow"] +CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"] +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True CONF_PIN_COUNT = "pin_count" diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 1166cc1a09..e8d49f66e2 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -37,10 +37,9 @@ void PCA9554Component::setup() { } void PCA9554Component::loop() { - // The read_inputs_() method will cache the input values from the chip. - this->read_inputs_(); - // Clear all the previously read flags. - this->was_previously_read_ = 0x00; + // Invalidate the cache at the start of each loop. + // The actual read will happen on demand when digital_read() is called + this->reset_pin_cache_(); } void PCA9554Component::dump_config() { @@ -54,21 +53,17 @@ void PCA9554Component::dump_config() { } } -bool PCA9554Component::digital_read(uint8_t pin) { - // Note: We want to try and avoid doing any I2C bus read transactions here - // to conserve I2C bus bandwidth. So what we do is check to see if we - // have seen a read during the time esphome is running this loop. If we have, - // we do an I2C bus transaction to get the latest value. If we haven't - // we return a cached value which was read at the time loop() was called. - if (this->was_previously_read_ & (1 << pin)) - this->read_inputs_(); // Force a read of a new value - // Indicate we saw a read request for this pin in case a - // read happens later in the same loop. - this->was_previously_read_ |= (1 << pin); +bool PCA9554Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_inputs_(); // Return true if I2C read succeeded, false on error +} + +bool PCA9554Component::digital_read_cache(uint8_t pin) { + // Return the cached pin state from input_mask_ return this->input_mask_ & (1 << pin); } -void PCA9554Component::digital_write(uint8_t pin, bool value) { +void PCA9554Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { @@ -127,8 +122,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } -// Run our loop() method very early in the loop, so that we cache read values before -// before other components call our digital_read() method. +// Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI void PCA9554GPIOPin::setup() { pin_mode(flags_); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index efeec4d306..7b356b4068 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -3,22 +3,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca9554 { -class PCA9554Component : public Component, public i2c::I2CDevice { +class PCA9554Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA9554Component() = default; /// Check i2c availability and setup masks void setup() override; - /// Poll for input changes periodically + /// Invalidate cache at start of each loop void loop() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -32,9 +31,13 @@ class PCA9554Component : public Component, public i2c::I2CDevice { protected: bool read_inputs_(); - bool write_register_(uint8_t reg, uint16_t value); + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + /// number of bits the expander has size_t pin_count_{8}; /// width of registers @@ -45,8 +48,6 @@ class PCA9554Component : public Component, public i2c::I2CDevice { uint16_t output_mask_{0x00}; /// The state of the actual input pin states - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; - /// Flags to check if read previously during this loop - uint16_t was_previously_read_ = {0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; }; diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index ff7c314bcd..f387d0a610 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 848fbed484..72d8865d7f 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -16,6 +16,10 @@ void PCF8574Component::setup() { this->write_gpio_(); this->read_gpio_(); } +void PCF8574Component::loop() { + // Invalidate the cache at the start of each loop + this->reset_pin_cache_(); +} void PCF8574Component::dump_config() { ESP_LOGCONFIG(TAG, "PCF8574:"); LOG_I2C_DEVICE(this) @@ -24,17 +28,19 @@ void PCF8574Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } } -bool PCF8574Component::digital_read(uint8_t pin) { - this->read_gpio_(); - return this->input_mask_ & (1 << pin); +bool PCF8574Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_gpio_(); // Return true if I2C read succeeded, false on error } -void PCF8574Component::digital_write(uint8_t pin, bool value) { + +bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCF8574Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { this->output_mask_ &= ~(1 << pin); } - this->write_gpio_(); } void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { @@ -91,6 +97,9 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +// Run our loop() method early to invalidate cache before any other components access the pins +float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 6edc67fc96..fd1ea8af63 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -3,11 +3,16 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pcf8574 { -class PCF8574Component : public Component, public i2c::I2CDevice { +// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction +// so we use uint16_t as bank type to ensure all pins are in one bank and cached together +class PCF8574Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCF8574Component() = default; @@ -15,20 +20,22 @@ class PCF8574Component : public Component, public i2c::I2CDevice { /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + /// Invalidate cache at start of each loop + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; + float get_loop_priority() const override; void dump_config() override; protected: - bool read_gpio_(); + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_gpio_(); bool write_gpio_(); /// Mask for the pin mode - 1 means output, 0 means input diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 18acfda934..517ca833e6 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -68,7 +68,7 @@ bool PI4IOE5V6408Component::read_gpio_outputs_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = data; @@ -82,7 +82,7 @@ bool PI4IOE5V6408Component::read_gpio_modes_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { - this->status_set_warning("Failed to read GPIO modes"); + this->status_set_warning(LOG_STR("Failed to read GPIO modes")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -99,7 +99,7 @@ bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { - this->status_set_warning("Failed to read GPIO state"); + this->status_set_warning(LOG_STR("Failed to read GPIO state")); return false; } this->input_mask_ = data; @@ -117,7 +117,7 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { this->output_mask_ &= ~(1 << pin); } if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -131,15 +131,15 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { return false; if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { - this->status_set_warning("Failed to write GPIO modes"); + this->status_set_warning(LOG_STR("Failed to write GPIO modes")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { - this->status_set_warning("Failed to write GPIO pullup/pulldown"); + this->status_set_warning(LOG_STR("Failed to write GPIO pullup/pulldown")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { - this->status_set_warning("Failed to write GPIO pull enable"); + this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index c086ceaa23..d083d48b32 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -18,7 +18,7 @@ void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { void PulseWidthSensor::dump_config() { LOG_SENSOR("", "Pulse Width", this); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_PIN(" Pin: ", this->pin_); } void PulseWidthSensor::update() { diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 8f5d5daf01..f95be5291f 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -17,16 +17,8 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t if (component == nullptr) return; - // Check if we have cached the name for this component - auto name_it = this->component_names_cache_.find(component); - if (name_it == this->component_names_cache_.end()) { - // First time seeing this component, cache its name - const char *source = component->get_component_source(); - this->component_names_cache_[component] = source; - this->component_stats_[source].record_time(duration_ms); - } else { - this->component_stats_[name_it->second].record_time(duration_ms); - } + // Record stats using component pointer as key + this->component_stats_[component].record_time(duration_ms); if (this->next_log_time_ == 0) { this->next_log_time_ = current_time + this->log_interval_; @@ -42,9 +34,10 @@ void RuntimeStatsCollector::log_stats_() { std::vector stats_to_display; for (const auto &it : this->component_stats_) { + Component *component = it.first; const ComponentRuntimeStats &stats = it.second; if (stats.get_period_count() > 0) { - ComponentStatPair pair = {it.first, &stats}; + ComponentStatPair pair = {component, &stats}; stats_to_display.push_back(pair); } } @@ -54,12 +47,9 @@ void RuntimeStatsCollector::log_stats_() { // Log top components by period runtime for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(), - stats->get_period_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), + it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); } // Log total stats since boot @@ -72,12 +62,9 @@ void RuntimeStatsCollector::log_stats_() { }); for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), - stats->get_total_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), + it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index e2f8bee563..56122364c2 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -79,7 +79,7 @@ class ComponentRuntimeStats { // For sorting components by run time struct ComponentStatPair { - const char *name; + Component *component; const ComponentRuntimeStats *stats; bool operator>(const ComponentStatPair &other) const { @@ -109,15 +109,9 @@ class RuntimeStatsCollector { } } - // Use const char* keys for efficiency - // Custom comparator for const char* keys in map - // Without this, std::map would compare pointer addresses instead of string contents, - // causing identical component names at different addresses to be treated as different keys - struct CStrCompare { - bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } - }; - std::map component_stats_; - std::map component_names_cache_; + // Map from component to its stats + // We use Component* as the key since each component is unique + std::map component_stats_; uint32_t log_interval_; uint32_t next_log_time_; }; diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp index 331f7dcd65..81f652d26a 100644 --- a/esphome/components/script/script.cpp +++ b/esphome/components/script/script.cpp @@ -6,9 +6,15 @@ namespace script { static const char *const TAG = "script"; +#ifdef USE_STORE_LOG_STR_IN_FLASH +void ScriptLogger::esp_log_(int level, int line, const __FlashStringHelper *format, const char *param) { + esp_log_printf_(level, TAG, line, format, param); +} +#else void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) { esp_log_printf_(level, TAG, line, format, param); } +#endif } // namespace script } // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 60175ec933..b16bb53acc 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -10,6 +10,15 @@ namespace script { class ScriptLogger { protected: +#ifdef USE_STORE_LOG_STR_IN_FLASH + void esp_logw_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); + } + void esp_logd_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); + } + void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param); +#else void esp_logw_(int line, const char *format, const char *param) { esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); } @@ -17,6 +26,7 @@ class ScriptLogger { esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); } void esp_log_(int level, int line, const char *format, const char *param); +#endif }; /// The abstract base class for all script types. @@ -57,7 +67,8 @@ template class SingleScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logw_(__LINE__, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"), + this->name_.c_str()); return; } @@ -74,7 +85,7 @@ template class RestartScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logd_(__LINE__, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), this->name_.c_str()); this->stop_action(); } @@ -93,11 +104,13 @@ template class QueueingScript : public Script, public Com // num_runs_ is the number of *queued* instances, so total number of instances is // num_runs_ + 1 if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"), + this->name_.c_str()); return; } - this->esp_logd_(__LINE__, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"), + this->name_.c_str()); this->num_runs_++; this->var_queue_.push(std::make_tuple(x...)); return; @@ -143,7 +156,8 @@ template class ParallelScript : public Script { public: void execute(Ts... x) override { if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"), + this->name_.c_str()); return; } this->trigger(x...); diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 0f27ec1b10..3298a5b8db 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -29,6 +29,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { + switch (mode) { + case LOW_ACCELERATION: + return LOG_STR("LOW"); + case MEDIUM_ACCELERATION: + return LOG_STR("MEDIUM"); + case HIGH_ACCELERATION: + return LOG_STR("HIGH"); + default: + return LOG_STR("UNKNOWN"); + } +} + void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { @@ -38,6 +51,7 @@ void SEN5XComponent::setup() { this->mark_failed(); return; } + delay(20); // per datasheet uint16_t raw_read_status; if (!this->read_data(raw_read_status)) { @@ -49,7 +63,7 @@ void SEN5XComponent::setup() { uint32_t stop_measurement_delay = 0; // In order to query the device periodic measurement must be ceased if (raw_read_status) { - ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + ESP_LOGD(TAG, "Data is available; stopping periodic measurement"); if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); @@ -70,7 +84,8 @@ void SEN5XComponent::setup() { this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); - ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); uint16_t raw_product_name[16]; if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { @@ -87,45 +102,43 @@ void SEN5XComponent::setup() { // first char current_char = *current_int >> 8; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); // second char current_char = *current_int & 0xFF; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); } } current_int++; } while (current_char && --max); Sen5xType sen5x_type = UNKNOWN; - if (product_name_ == "SEN50") { + if (this->product_name_ == "SEN50") { sen5x_type = SEN50; } else { - if (product_name_ == "SEN54") { + if (this->product_name_ == "SEN54") { sen5x_type = SEN54; } else { - if (product_name_ == "SEN55") { + if (this->product_name_ == "SEN55") { sen5x_type = SEN55; } } - ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); + ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); } if (this->humidity_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used } if (this->temperature_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55"); this->temperature_sensor_ = nullptr; // mark as not used } if (this->voc_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55"); this->voc_sensor_ = nullptr; // mark as not used } if (this->nox_sensor_ && sen5x_type != SEN55) { - ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "NOx requires a SEN55"); this->nox_sensor_ = nullptr; // mark as not used } @@ -136,7 +149,7 @@ void SEN5XComponent::setup() { return; } this->firmware_version_ >>= 8; - ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); + ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { uint32_t combined_serial = @@ -149,7 +162,7 @@ void SEN5XComponent::setup() { if (this->pref_.load(&this->voc_baselines_storage_)) { ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } // Initialize storage timestamp @@ -157,13 +170,13 @@ void SEN5XComponent::setup() { if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); uint16_t states[4]; - states[0] = voc_baselines_storage_.state0 >> 16; - states[1] = voc_baselines_storage_.state0 & 0xFFFF; - states[2] = voc_baselines_storage_.state1 >> 16; - states[3] = voc_baselines_storage_.state1 & 0xFFFF; + states[0] = this->voc_baselines_storage_.state0 >> 16; + states[1] = this->voc_baselines_storage_.state0 & 0xFFFF; + states[2] = this->voc_baselines_storage_.state1 >> 16; + states[3] = this->voc_baselines_storage_.state1 & 0xFFFF; if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); @@ -181,11 +194,11 @@ void SEN5XComponent::setup() { delay(20); uint16_t secs[2]; if (this->read_data(secs, 2)) { - auto_cleaning_interval_ = secs[0] << 16 | secs[1]; + this->auto_cleaning_interval_ = secs[0] << 16 | secs[1]; } } - if (acceleration_mode_.has_value()) { - result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); + if (this->acceleration_mode_.has_value()) { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value()); } else { result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); } @@ -196,7 +209,7 @@ void SEN5XComponent::setup() { return; } delay(20); - if (!acceleration_mode_.has_value()) { + if (!this->acceleration_mode_.has_value()) { uint16_t mode; if (this->read_data(mode)) { this->acceleration_mode_ = RhtAccelerationMode(mode); @@ -226,19 +239,18 @@ void SEN5XComponent::setup() { } if (!this->write_command(cmd)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); + ESP_LOGE(TAG, "Error starting continuous measurements"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - initialized_ = true; - ESP_LOGD(TAG, "Sensor initialized"); + this->initialized_ = true; }); }); } void SEN5XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "sen5x:"); + ESP_LOGCONFIG(TAG, "SEN5X:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -246,16 +258,16 @@ void SEN5XComponent::dump_config() { ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); break; case MEASUREMENT_INIT_FAILED: - ESP_LOGW(TAG, "Measurement Initialization failed"); + ESP_LOGW(TAG, "Measurement initialization failed"); break; case SERIAL_NUMBER_IDENTIFICATION_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial id"); + ESP_LOGW(TAG, "Unable to read serial ID"); break; case PRODUCT_NAME_FAILED: ESP_LOGW(TAG, "Unable to read product name"); break; case FIRMWARE_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -263,26 +275,17 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Productname: %s\n" + " Product name: %s\n" " Firmware version: %d\n" " Serial number %02d.%02d.%02d", - this->product_name_.c_str(), this->firmware_version_, serial_number_[0], serial_number_[1], - serial_number_[2]); + this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); if (this->auto_cleaning_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Auto cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value()); + ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); } if (this->acceleration_mode_.has_value()) { - switch (this->acceleration_mode_.value()) { - case LOW_ACCELERATION: - ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); - break; - case MEDIUM_ACCELERATION: - ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode"); - break; - case HIGH_ACCELERATION: - ESP_LOGCONFIG(TAG, " High RH/T acceleration mode"); - break; - } + ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", + LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); @@ -296,7 +299,7 @@ void SEN5XComponent::dump_config() { } void SEN5XComponent::update() { - if (!initialized_) { + if (!this->initialized_) { return; } @@ -319,8 +322,8 @@ void SEN5XComponent::update() { this->voc_baselines_storage_.state1 = state1; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Could not store VOC baselines"); } @@ -332,7 +335,7 @@ void SEN5XComponent::update() { if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { this->status_set_warning(); - ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); + ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); return; } this->set_timeout(20, [this]() { @@ -340,7 +343,7 @@ void SEN5XComponent::update() { if (!this->read_data(measurements, 8)) { this->status_set_warning(); - ESP_LOGD(TAG, "read data error (%d)", this->last_error_); + ESP_LOGD(TAG, "Read data error (%d)", this->last_error_); return; } @@ -412,7 +415,7 @@ bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTun params[5] = tuning.gain_factor; auto result = write_command(i2c_command, params, 6); if (!result) { - ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); + ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_); } return result; } @@ -423,7 +426,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati params[1] = compensation.normalized_offset_slope; params[2] = compensation.time_constant; if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { - ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); + ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_); return false; } return true; @@ -432,7 +435,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati bool SEN5XComponent::start_fan_cleaning() { if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 0fa31605e6..9e5b6bf231 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -9,7 +9,7 @@ namespace esphome { namespace sen5x { -enum ERRORCODE { +enum ERRORCODE : uint8_t { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, @@ -18,19 +18,17 @@ enum ERRORCODE { UNKNOWN }; -// Shortest time interval of 3H for storing baseline values. -// Prevents wear of the flash because of too many write operations -const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -// Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; +enum RhtAccelerationMode : uint16_t { + LOW_ACCELERATION = 0, + MEDIUM_ACCELERATION = 1, + HIGH_ACCELERATION = 2, +}; struct Sen5xBaselines { int32_t state0; int32_t state1; } PACKED; // NOLINT -enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; - struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -46,6 +44,12 @@ struct TemperatureCompensation { uint16_t time_constant; }; +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +// Store anyway if the baseline difference exceeds the max storage diff value +static const uint32_t MAXIMUM_STORAGE_DIFF = 50; + class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void setup() override; @@ -102,8 +106,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri protected: bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_temperature_compensation_(const TemperatureCompensation &compensation); + + uint32_t seconds_since_last_store_; + uint16_t firmware_version_; ERRORCODE error_code_; + uint8_t serial_number_[4]; bool initialized_{false}; + bool store_baseline_; + sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_4_0_sensor_{nullptr}; @@ -115,18 +125,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri // SEN55 only sensor::Sensor *nox_sensor_{nullptr}; - std::string product_name_; - uint8_t serial_number_[4]; - uint16_t firmware_version_; - Sen5xBaselines voc_baselines_storage_; - bool store_baseline_; - uint32_t seconds_since_last_store_; - ESPPreferenceObject pref_; optional acceleration_mode_; optional auto_cleaning_interval_; optional voc_tuning_params_; optional nox_tuning_params_; optional temperature_compensation_; + ESPPreferenceObject pref_; + std::string product_name_; + Sen5xBaselines voc_baselines_storage_; }; } // namespace sen5x diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index f71b3c14cb..22c4b0e53c 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -11,21 +11,22 @@ static const char *const TAG = "sensirion_i2c"; // To avoid memory allocations for small writes a stack buffer is used static const size_t BUFFER_STACK_SIZE = 16; -bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { +bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); + uint8_t buf[num_bytes]; - last_error_ = this->read(buf.data(), num_bytes); - if (last_error_ != i2c::ERROR_OK) { + this->last_error_ = this->read(buf, num_bytes); + if (this->last_error_ != i2c::ERROR_OK) { return false; } for (uint8_t i = 0; i < len; i++) { const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + uint8_t crc = crc8(&buf[j], 2, 0xFF, CRC_POLYNOMIAL, true); if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); - last_error_ = i2c::ERROR_CRC; + ESP_LOGE(TAG, "CRC invalid @ %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + this->last_error_ = i2c::ERROR_CRC; return false; } data[i] = encode_uint16(buf[j], buf[j + 1]); @@ -34,10 +35,10 @@ bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { } /*** * write command with parameters and insert crc - * use stack array for less than 4 parameters. Most sensirion i2c commands have less parameters + * use stack array for less than 4 parameters. Most Sensirion I2C commands have less parameters */ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, - uint8_t data_len) { + const uint8_t data_len) { uint8_t temp_stack[BUFFER_STACK_SIZE]; std::unique_ptr temp_heap; uint8_t *temp; @@ -74,56 +75,26 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len temp[raw_idx++] = data[i] & 0xFF; temp[raw_idx++] = data[i] >> 8; #endif - temp[raw_idx++] = sht_crc_(data[i]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + temp[raw_idx++] = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); } - last_error_ = this->write(temp, raw_idx); - return last_error_ == i2c::ERROR_OK; + this->last_error_ = this->write(temp, raw_idx); + return this->last_error_ == i2c::ERROR_OK; } -bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, - uint8_t delay_ms) { +bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, const uint8_t len, + const uint8_t delay_ms) { if (!this->write_command_(reg, command_len, nullptr, 0)) { - ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_); + ESP_LOGE(TAG, "Write failed: reg=0x%X (%d) err=%d,", reg, command_len, this->last_error_); return false; } delay(delay_ms); bool result = this->read_data(data, len); if (!result) { - ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_); + ESP_LOGE(TAG, "Read failed: reg=0x%X err=%d,", reg, this->last_error_); } return result; } -// The 8-bit CRC checksum is transmitted after each data word -uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) { - uint8_t bit; - uint8_t crc = 0xFF; -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data >> 8; -#else - crc ^= data & 0xFF; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data & 0xFF; -#else - crc ^= data >> 8; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } - return crc; -} - } // namespace sensirion_common } // namespace esphome diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index aba93d6cc3..f3eb3761f6 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -8,90 +8,92 @@ namespace esphome { namespace sensirion_common { /** - * Implementation of a i2c functions for Sensirion sensors - * Sensirion data requires crc checking. + * Implementation of I2C functions for Sensirion sensors + * Sensirion data requires CRC checking. * Each 16 bit word is/must be followed 8 bit CRC code - * (Applies to read and write - note the i2c command code doesn't need a CRC) + * (Applies to read and write - note the I2C command code doesn't need a CRC) * Format: * | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | .. */ +static const uint8_t CRC_POLYNOMIAL = 0x31; // default for Sensirion + class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; - /** Read data words from i2c device. - * handles crc check used by Sensirion sensors + /** Read data words from I2C device. + * handles CRC check used by Sensirion sensors * @param data pointer to raw result * @param len number of words to read * @return true if reading succeeded */ bool read_data(uint16_t *data, uint8_t len); - /** Read 1 data word from i2c device. + /** Read 1 data word from I2C device. * @param data reference to raw result * @return true if reading succeeded */ bool read_data(uint16_t &data) { return this->read_data(&data, 1); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(command, ADDR_16_BIT, data, len, delay); } - /** Read 1 data word from 16 bit i2c register. - * @param i2c register + /** Read 1 data word from 16 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(i2c_register, ADDR_8_BIT, data, len, delay); } - /** Read 1 data word from 8 bit i2c register. - * @param i2c register + /** Read 1 data word from 8 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay); } - /** Write a command to the i2c device. - * @param command i2c command to send + /** Write a command to the I2C device. + * @param command I2C command to send * @return true if reading succeeded */ template bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); } - /** Write a command and one data word to the i2c device . - * @param command i2c command to send - * @param data argument for the i2c command + /** Write a command and one data word to the I2C device . + * @param command I2C command to send + * @param data argument for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data vector arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data vector arguments for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, const std::vector &data) { @@ -99,57 +101,39 @@ class SensirionI2CDevice : public i2c::I2CDevice { } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data arguments for the I2C command * @param len number of arguments (words) * @return true if reading succeeded */ template bool write_command(T i2c_register, const uint16_t *data, uint8_t len) { // limit to 8 or 16 bit only - static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, - "only 8 or 16 bit command types are supported."); + static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, "Only 8 or 16 bit command types supported"); return write_command_(i2c_register, CommandLen(sizeof(T)), data, len); } protected: - uint8_t crc_polynomial_{0x31u}; // default for sensirion /** Write a command with arguments as words - * @param command i2c command to send can be uint8_t or uint16_t + * @param command I2C command to send can be uint8_t or uint16_t * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes - * @param data arguments for the i2c command + * @param data arguments for the I2C command * @param data_len number of arguments (words) * @return true if reading succeeded */ bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len); - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay); - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data data word for which the crc8 checksum is calculated - * @param len number of arguments (words) - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint16_t data); - - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data1 high byte of data word - * @param data2 low byte of data word - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); } - - /** last error code from i2c operation + /** last error code from I2C operation */ i2c::ErrorCode last_error_; }; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index e2e8302d8b..4292b8c0bc 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -17,7 +17,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o "%s State Class: '%s'\n" "%s Unit of Measurement: '%s'\n" "%s Accuracy Decimals: %d", - prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, + prefix, type, obj->get_name().c_str(), prefix, + LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); if (!obj->get_device_class_ref().empty()) { @@ -33,17 +34,17 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } -const char *state_class_to_string(StateClass state_class) { +const LogString *state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: - return "measurement"; + return LOG_STR("measurement"); case STATE_CLASS_TOTAL_INCREASING: - return "total_increasing"; + return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: - return "total"; + return LOG_STR("total"); case STATE_CLASS_NONE: default: - return ""; + return LOG_STR(""); } } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 507cb326b2..f3fa601a5e 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -33,7 +33,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, }; -const char *state_class_to_string(StateClass state_class); +const LogString *state_class_to_string(StateClass state_class); /** Base-class for all sensors. * diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 42baff6d23..0e3aeff812 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,9 +1,11 @@ #include "sgp30.h" -#include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace sgp30 { @@ -39,9 +41,8 @@ void SGP30Component::setup() { this->mark_failed(); return; } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | - (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); + this->serial_number_ = encode_uint24(raw_serial_number[0], raw_serial_number[1], raw_serial_number[2]); + ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); // Featureset identification for future use uint16_t raw_featureset; @@ -61,11 +62,11 @@ void SGP30Component::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + ESP_LOGV(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); // Sensor initialization if (!this->write_command(SGP30_CMD_IAQ_INIT)) { - ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); + ESP_LOGE(TAG, "sgp30_iaq_init failed"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; @@ -123,7 +124,7 @@ void SGP30Component::read_iaq_baseline_() { uint16_t eco2baseline = (raw_data[0]); uint16_t tvocbaseline = (raw_data[1]); - ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); + ESP_LOGI(TAG, "Baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2baseline, tvocbaseline); if (eco2baseline != this->eco2_baseline_ || tvocbaseline != this->tvoc_baseline_) { this->eco2_baseline_ = eco2baseline; this->tvoc_baseline_ = tvocbaseline; @@ -142,7 +143,7 @@ void SGP30Component::read_iaq_baseline_() { this->baselines_storage_.eco2 = this->eco2_baseline_; this->baselines_storage_.tvoc = this->tvoc_baseline_; if (this->pref_.save(&this->baselines_storage_)) { - ESP_LOGI(TAG, "Store eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + ESP_LOGI(TAG, "Store baselines: eCO2: 0x%04X, TVOC: 0x%04X", this->baselines_storage_.eco2, this->baselines_storage_.tvoc); } else { ESP_LOGW(TAG, "Could not store eCO2 and TVOC baselines"); @@ -164,7 +165,7 @@ void SGP30Component::send_env_data_() { if (this->humidity_sensor_ != nullptr) humidity = this->humidity_sensor_->state; if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data"); return; } else { ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); @@ -174,7 +175,7 @@ void SGP30Component::send_env_data_() { temperature = float(this->temperature_sensor_->state); } if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value"); return; } else { ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); @@ -192,18 +193,17 @@ void SGP30Component::send_env_data_() { ((humidity * 0.061121f * std::exp((18.678f - temperature / 234.5f) * (temperature / (257.14f + temperature)))) / (273.15f + temperature)); } - uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); - uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); - ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, - uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); - uint8_t crc = sht_crc_(humidity_full, humidity_dec); - uint8_t data[4]; - data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; - data[1] = humidity_full; - data[2] = humidity_dec; - data[3] = crc; + uint8_t data[4] = { + SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF, + uint8_t(std::floor(absolute_humidity)), // humidity_full + uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)), // humidity_dec + 0, + }; + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); + ESP_LOGD(TAG, "Calculated absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + encode_uint16(data[1], data[2])); if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { - ESP_LOGE(TAG, "Error sending compensation data."); + ESP_LOGE(TAG, "Error sending compensation data"); } } @@ -212,15 +212,14 @@ void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_b data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; data[1] = tvoc_baseline >> 8; data[2] = tvoc_baseline & 0xFF; - data[3] = sht_crc_(data[1], data[2]); + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); data[4] = eco2_baseline >> 8; data[5] = eco2_baseline & 0xFF; - data[6] = sht_crc_(data[4], data[5]); + data[6] = crc8(&data[4], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { - ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); + ESP_LOGE(TAG, "Error applying baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } else { - ESP_LOGI(TAG, "Initial baselines applied successfully! eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, - tvoc_baseline); + ESP_LOGI(TAG, "Initial baselines applied: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } } @@ -236,10 +235,10 @@ void SGP30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case INVALID_ID: - ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + ESP_LOGW(TAG, "Invalid ID"); break; case UNSUPPORTED_ID: - ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)"); + ESP_LOGW(TAG, "Unsupported ID"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -249,12 +248,12 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { ESP_LOGCONFIG(TAG, - " Baseline:\n" - " eCO2 Baseline: 0x%04X\n" - " TVOC Baseline: 0x%04X", + " Baselines:\n" + " eCO2: 0x%04X\n" + " TVOC: 0x%04X", this->eco2_baseline_, this->tvoc_baseline_); } else { - ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); + ESP_LOGCONFIG(TAG, " Baselines not configured"); } ESP_LOGCONFIG(TAG, " Warm up time: %" PRIu32 "s", this->required_warm_up_time_); } @@ -266,8 +265,8 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, "Store baseline: %s", YESNO(this->store_baseline_)); if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); - LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); - LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity source:", this->humidity_sensor_); } else { ESP_LOGCONFIG(TAG, " Compensation: No source configured"); } @@ -289,7 +288,7 @@ void SGP30Component::update() { float eco2 = (raw_data[0]); float tvoc = (raw_data[1]); - ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + ESP_LOGV(TAG, "eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); if (this->eco2_sensor_ != nullptr) this->eco2_sensor_->publish_state(eco2); if (this->tvoc_sensor_ != nullptr) diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index e6429a7bfa..4648a33e15 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" #include "esphome/core/preferences.h" #include @@ -38,14 +38,16 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); + uint64_t serial_number_; - uint16_t featureset_; uint32_t required_warm_up_time_; uint32_t seconds_since_last_store_; - SGP30Baselines baselines_storage_; - ESPPreferenceObject pref_; + uint16_t featureset_; + uint16_t eco2_baseline_{0x0000}; + uint16_t tvoc_baseline_{0x0000}; + bool store_baseline_; - enum ErrorCode { + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, MEASUREMENT_INIT_FAILED, INVALID_ID, @@ -53,14 +55,13 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri UNKNOWN } error_code_{UNKNOWN}; + ESPPreferenceObject pref_; + SGP30Baselines baselines_storage_; + sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; sensor::Sensor *eco2_sensor_baseline_{nullptr}; sensor::Sensor *tvoc_sensor_baseline_{nullptr}; - uint16_t eco2_baseline_{0x0000}; - uint16_t tvoc_baseline_{0x0000}; - bool store_baseline_; - /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index da52993a87..99d88006f7 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -211,7 +211,7 @@ void SGP4xComponent::measure_raw_() { if (!this->write_command(command, data, 2)) { ESP_LOGD(TAG, "write error (%d)", this->last_error_); - this->status_set_warning("measurement request failed"); + this->status_set_warning(LOG_STR("measurement request failed")); return; } @@ -220,7 +220,7 @@ void SGP4xComponent::measure_raw_() { raw_data[1] = 0; if (!this->read_data(raw_data, response_words)) { ESP_LOGD(TAG, "read error (%d)", this->last_error_); - this->status_set_warning("measurement read failed"); + this->status_set_warning(LOG_STR("measurement read failed")); this->voc_index_ = this->nox_index_ = UINT16_MAX; return; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 637c8c1a9d..62b8717ded 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -65,7 +65,7 @@ void SHT4XComponent::update() { // Send command if (!this->write_command(MEASURECOMMANDS[this->precision_])) { // Warning will be printed only if warning status is not set yet - this->status_set_warning("Failed to send measurement command"); + this->status_set_warning(LOG_STR("Failed to send measurement command")); return; } diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index decf630aba..db6b168bbc 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -56,7 +56,7 @@ void SoundLevelComponent::loop() { } } else { if (!this->status_has_warning()) { - this->status_set_warning("Microphone isn't running, can't compute statistics"); + this->status_set_warning(LOG_STR("Microphone isn't running, can't compute statistics")); // Deallocate buffers, if necessary this->stop_(); diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 272acc78f2..b99bf416d6 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -43,20 +43,20 @@ void SPS30Component::setup() { this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } - ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); bool result; if (this->fan_interval_.has_value()) { // override default value - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); } else { - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } if (result) { delay(20); uint16_t secs[2]; if (this->read_data(secs, 2)) { - fan_interval_ = secs[0] << 16 | secs[1]; + this->fan_interval_ = secs[0] << 16 | secs[1]; } } @@ -67,7 +67,7 @@ void SPS30Component::setup() { } void SPS30Component::dump_config() { - ESP_LOGCONFIG(TAG, "sps30:"); + ESP_LOGCONFIG(TAG, "SPS30:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -78,16 +78,16 @@ void SPS30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case SERIAL_NUMBER_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor serial number"); + ESP_LOGW(TAG, "Unable to request serial number"); break; case SERIAL_NUMBER_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial number"); + ESP_LOGW(TAG, "Unable to read serial number"); break; case FIRMWARE_VERSION_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor firmware version"); + ESP_LOGW(TAG, "Unable to request firmware version"); break; case FIRMWARE_VERSION_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -96,9 +96,9 @@ void SPS30Component::dump_config() { } LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, - " Serial Number: '%s'\n" + " Serial number: %s\n" " Firmware version v%0d.%0d", - this->serial_number_, (raw_firmware_version_ >> 8), uint16_t(raw_firmware_version_ & 0xFF)); + this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); @@ -113,15 +113,15 @@ void SPS30Component::dump_config() { void SPS30Component::update() { /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { - ESP_LOGD(TAG, "Trying to reconnect"); + ESP_LOGD(TAG, "Reconnecting"); if (this->write_command(SPS30_CMD_SOFT_RESET)) { - ESP_LOGD(TAG, "Soft-reset successful. Waiting for reconnection in 500 ms"); + ESP_LOGD(TAG, "Soft-reset successful; waiting 500 ms"); this->set_timeout(500, [this]() { this->start_continuous_measurement_(); /// Sensor restarted and reading attempt made next cycle this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; - ESP_LOGD(TAG, "Reconnect successful. Resuming continuous measurement"); + ESP_LOGD(TAG, "Reconnected; resuming continuous measurement"); }); } else { ESP_LOGD(TAG, "Soft-reset failed"); @@ -136,12 +136,12 @@ void SPS30Component::update() { uint16_t raw_read_status; if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) { - ESP_LOGD(TAG, "Not ready yet"); + ESP_LOGD(TAG, "Not ready"); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { - ESP_LOGD(TAG, "Exceeded max allowed attempts; communication will be reinitialized"); + ESP_LOGD(TAG, "Exceeded max attempts; will reinitialize"); this->status_set_warning(); } return; @@ -211,11 +211,6 @@ void SPS30Component::update() { } bool SPS30Component::start_continuous_measurement_() { - uint8_t data[4]; - data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; - data[1] = 0x03; - data[2] = 0x00; - data[3] = sht_crc_(0x03, 0x00); if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) { ESP_LOGE(TAG, "Error initiating measurements"); return false; @@ -224,9 +219,9 @@ bool SPS30Component::start_continuous_measurement_() { } bool SPS30Component::start_fan_cleaning() { - if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { + if (!this->write_command(SPS30_CMD_START_FAN_CLEANING)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 04189247e8..461a770ab6 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,12 +30,12 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: - char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; - bool start_continuous_measurement_(); + char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; + bool start_continuous_measurement_(); - enum ErrorCode { + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, FIRMWARE_VERSION_REQUEST_FAILED, FIRMWARE_VERSION_READ_FAILED, diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 67dc924903..b61b92fd1e 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -25,7 +25,7 @@ CONF_SCAN_TIME = "scan_time" CONF_DEBOUNCE_TIME = "debounce_time" CONF_SX1509_ID = "sx1509_id" -AUTO_LOAD = ["key_provider"] +AUTO_LOAD = ["key_provider", "gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2bf6701dd2..746ec9cda3 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -39,6 +39,9 @@ void SX1509Component::dump_config() { } void SX1509Component::loop() { + // Reset cache at the start of each loop + this->reset_pin_cache_(); + if (this->has_keypad_) { if (millis() - this->last_loop_timestamp_ < min_loop_period_) return; @@ -73,18 +76,20 @@ void SX1509Component::loop() { } } -bool SX1509Component::digital_read(uint8_t pin) { +bool SX1509Component::digital_read_hw(uint8_t pin) { + // Always read all pins when any input pin is accessed + return this->read_byte_16(REG_DATA_B, &this->input_mask_); +} + +bool SX1509Component::digital_read_cache(uint8_t pin) { + // Return cached value for input pins, false for output pins if (this->ddr_mask_ & (1 << pin)) { - uint16_t temp_reg_data; - if (!this->read_byte_16(REG_DATA_B, &temp_reg_data)) - return false; - if (temp_reg_data & (1 << pin)) - return true; + return (this->input_mask_ & (1 << pin)) != 0; } return false; } -void SX1509Component::digital_write(uint8_t pin, bool bit_value) { +void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) { if ((~this->ddr_mask_) & (1 << pin)) { // If the pin is an output, write high/low uint16_t temp_reg_data = 0; diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index c0e86aa8a1..2afd0d0e4e 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -2,6 +2,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/key_provider/key_provider.h" +#include "esphome/components/gpio_expander/cached_gpio.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "sx1509_gpio_pin.h" @@ -30,7 +31,10 @@ class SX1509Processor { class SX1509KeyTrigger : public Trigger {}; -class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider { +class SX1509Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander, + public key_provider::KeyProvider { public: SX1509Component() = default; @@ -39,11 +43,9 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - bool digital_read(uint8_t pin); uint16_t read_key_data(); void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; void pin_mode(uint8_t pin, gpio::Flags flags); - void digital_write(uint8_t pin, bool bit_value); uint32_t get_clock() { return this->clk_x_; }; void set_rows_cols(uint8_t rows, uint8_t cols) { this->rows_ = rows; @@ -61,10 +63,15 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov void setup_led_driver(uint8_t pin); protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + uint32_t clk_x_ = 2000000; uint8_t frequency_ = 0; uint16_t ddr_mask_ = 0x00; - uint16_t input_mask_ = 0x00; + uint16_t input_mask_ = 0x00; // Cache for input values (16-bit for all pins) uint16_t port_mask_ = 0x00; uint16_t output_state_ = 0x00; bool has_keypad_ = false; diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index b4a04d5b0b..c3449ce254 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -50,7 +50,7 @@ bool TCA9555Component::read_gpio_outputs_() { return false; uint8_t data[2]; if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -64,7 +64,7 @@ bool TCA9555Component::read_gpio_modes_() { uint8_t data[2]; bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); if (!success) { - this->status_set_warning("Failed to read mode register"); + this->status_set_warning(LOG_STR("Failed to read mode register")); return false; } this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -79,7 +79,7 @@ bool TCA9555Component::digital_read_hw(uint8_t pin) { uint8_t bank_number = pin < 8 ? 0 : 1; uint8_t register_to_read = bank_number ? TCA9555_INPUT_PORT_REGISTER_1 : TCA9555_INPUT_PORT_REGISTER_0; if (!this->read_bytes(register_to_read, &data, 1)) { - this->status_set_warning("Failed to read input register"); + this->status_set_warning(LOG_STR("Failed to read input register")); return false; } uint8_t second_half = this->input_mask_ >> 8; @@ -108,7 +108,7 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) { data[0] = this->output_mask_; data[1] = this->output_mask_ >> 8; if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } @@ -123,7 +123,7 @@ bool TCA9555Component::write_gpio_modes_() { data[0] = this->mode_mask_; data[1] = this->mode_mask_ >> 8; if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) { - this->status_set_warning("Failed to write mode register"); + this->status_set_warning(LOG_STR("Failed to write mode register")); return false; } this->status_clear_warning(); diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 831f905bd2..1d9b384c66 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -32,7 +32,7 @@ void TMP1075Sensor::update() { uint16_t regvalue; if (!read_byte_16(REG_TEMP, ®value)) { ESP_LOGW(TAG, "'%s' - unable to read temperature register", this->name_.c_str()); - this->status_set_warning("can't read"); + this->status_set_warning(LOG_STR("can't read")); return; } this->status_clear_warning(); diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 62a1189355..8a9ce612b4 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -28,12 +28,12 @@ void UDPComponent::setup() { int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } } // create listening socket if we either want to subscribe to providers, or need to listen @@ -55,7 +55,7 @@ void UDPComponent::setup() { int enable = 1; err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } struct sockaddr_in server {}; diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 9e0055a2cc..0a57ecc67b 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -104,7 +104,7 @@ void UFireECComponent::write_data_(uint8_t reg, float data) { void UFireECComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-EC"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_); LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 9e0e7e265d..486a506391 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -141,7 +141,7 @@ void UFireISEComponent::write_data_(uint8_t reg, float data) { void UFireISEComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-ISE"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_); LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 934306f480..bf1c9086f1 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -266,7 +266,7 @@ void USBUartTypeCdcAcm::on_connected() { for (auto *channel : this->channels_) { if (i == cdc_devs.size()) { ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); - this->status_set_warning("No configuration found for channel"); + this->status_set_warning(LOG_STR("No configuration found for channel")); break; } channel->cdc_dev_ = cdc_devs[i++]; diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index bed098755a..adf5a080e5 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -74,12 +74,12 @@ void WakeOnLanButton::setup() { int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } #endif } diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 316cd80ccd..068cb91d31 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -181,7 +181,7 @@ void WaveshareEPaper2P13InV3::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_) LOG_PIN(" DC Pin: ", this->dc_pin_) LOG_PIN(" Busy Pin: ", this->busy_pin_) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); } void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7211f707e9..672a9868c5 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -198,9 +198,20 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { AsyncWebServerResponse *response; // Use the ota_success_ flag to determine the actual result +#ifdef USE_ESP8266 + static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!"; + static const char UPDATE_FAILED[] PROGMEM = "Update Failed!"; + static const char TEXT_PLAIN[] PROGMEM = "text/plain"; + static const char CONNECTION_STR[] PROGMEM = "Connection"; + static const char CLOSE_STR[] PROGMEM = "close"; + const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED; + response = request->beginResponse_P(200, TEXT_PLAIN, msg); + response->addHeader(CONNECTION_STR, CLOSE_STR); +#else const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!"; response = request->beginResponse(200, "text/plain", msg); response->addHeader("Connection", "close"); +#endif request->send(response); } diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d16c94fa13..e57bf25b8c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -148,7 +148,7 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { - this->status_set_warning("waiting to reconnect"); + this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { if (this->fast_connect_ || this->retry_hidden_) { if (!this->selected_ap_.get_bssid().has_value()) @@ -161,13 +161,13 @@ void WiFiComponent::loop() { break; } case WIFI_COMPONENT_STATE_STA_SCANNING: { - this->status_set_warning("scanning for networks"); + this->status_set_warning(LOG_STR("scanning for networks")); this->check_scanning_finished(); break; } case WIFI_COMPONENT_STATE_STA_CONNECTING: case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { - this->status_set_warning("associating to network"); + this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index dc745a2a46..1be193bb7e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -34,37 +34,20 @@ namespace esphome { static const char *const TAG = "app"; -// Helper function for insertion sort of components by setup priority +// Helper function for insertion sort of components by priority // Using insertion sort instead of std::stable_sort saves ~1.3KB of flash // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements), // which is necessary to maintain user-defined component order for same priority -template static void insertion_sort_by_setup_priority(Iterator first, Iterator last) { +template +static void insertion_sort_by_priority(Iterator first, Iterator last) { for (auto it = first + 1; it != last; ++it) { auto key = *it; - float key_priority = key->get_actual_setup_priority(); + float key_priority = (key->*GetPriority)(); auto j = it - 1; // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_actual_setup_priority() < key_priority) { - *(j + 1) = *j; - j--; - } - *(j + 1) = key; - } -} - -// Helper function for insertion sort of components by loop priority -// IMPORTANT: This sort is stable (preserves relative order of equal elements), -// which is required when components are re-sorted during setup() if they block -template static void insertion_sort_by_loop_priority(Iterator first, Iterator last) { - for (auto it = first + 1; it != last; ++it) { - auto key = *it; - float key_priority = key->get_loop_priority(); - auto j = it - 1; - - // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_loop_priority() < key_priority) { + while (j >= first && ((*j)->*GetPriority)() < key_priority) { *(j + 1) = *j; j--; } @@ -80,7 +63,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", LOG_STR_ARG(c->get_component_log_str()), c); return; } } @@ -91,7 +74,8 @@ void Application::setup() { ESP_LOGV(TAG, "Sorting components by setup priority"); // Sort by setup priority using our helper function - insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end()); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_actual_setup_priority>( + this->components_.begin(), this->components_.end()); // Initialize looping_components_ early so enable_pending_loops_() works during setup this->calculate_looping_components_(); @@ -108,7 +92,8 @@ void Application::setup() { continue; // Sort components 0 through i by loop priority - insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( + this->components_.begin(), this->components_.begin() + i + 1); do { uint8_t new_app_state = STATUS_LED_WARNING; @@ -340,8 +325,8 @@ void Application::teardown_components(uint32_t timeout_ms) { // Note: At this point, connections are either disconnected or in a bad state, // so this warning will only appear via serial rather than being transmitted to clients for (size_t i = 0; i < pending_count; ++i) { - ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", pending_components[i]->get_component_source(), - timeout_ms); + ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", + LOG_STR_ARG(pending_components[i]->get_component_log_str()), timeout_ms); } } } @@ -361,20 +346,19 @@ void Application::calculate_looping_components_() { // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization // before setup runs, so we need to respect their LOOP_DONE state - for (auto *obj : this->components_) { - if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - this->looping_components_.push_back(obj); - } - } + this->add_looping_components_by_state_(false); this->looping_components_active_end_ = this->looping_components_.size(); // Then add any components that are already LOOP_DONE to the inactive section // This handles components that called disable_loop() during initialization + this->add_looping_components_by_state_(true); +} + +void Application::add_looping_components_by_state_(bool match_loop_done) { for (auto *obj : this->components_) { if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + ((obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) == match_loop_done) { this->looping_components_.push_back(obj); } } @@ -473,7 +457,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", LOG_STR_ARG(component->get_component_log_str())); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; diff --git a/esphome/core/application.h b/esphome/core/application.h index 9cb2a4c638..1f22499051 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -431,6 +431,7 @@ class Application { void register_component_(Component *comp); void calculate_looping_components_(); + void add_looping_components_by_state_(bool match_loop_done); // These methods are called by Component::disable_loop() and Component::enable_loop() // Components should not call these directly - use this->disable_loop() or this->enable_loop() diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 40cda17ca3..ce4e2bf788 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -16,7 +16,6 @@ namespace esphome { static const char *const TAG = "component"; -static const char *const UNSPECIFIED_MESSAGE = "unspecified"; // Global vectors for component data that doesn't belong in every instance. // Using vector instead of unordered_map for both because: @@ -142,8 +141,8 @@ void Component::call_dump_config() { } } } - ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), - error_msg ? error_msg : UNSPECIFIED_MESSAGE); + ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), + error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); } } @@ -154,14 +153,14 @@ void Component::call() { case COMPONENT_STATE_CONSTRUCTION: { // State Construction: Call setup and set state to setup this->set_component_state_(COMPONENT_STATE_SETUP); - ESP_LOGV(TAG, "Setup %s", this->get_component_source()); + ESP_LOGV(TAG, "Setup %s", LOG_STR_ARG(this->get_component_log_str())); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t start_time = millis(); #endif this->call_setup(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t setup_time = millis() - start_time; - ESP_LOGCONFIG(TAG, "Setup %s took %ums", this->get_component_source(), (unsigned) setup_time); + ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); #endif break; } @@ -182,10 +181,8 @@ void Component::call() { break; } } -const char *Component::get_component_source() const { - if (this->component_source_ == nullptr) - return ""; - return this->component_source_; +const LogString *Component::get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; } bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { @@ -201,7 +198,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); + ESP_LOGE(TAG, "%s was marked as failed", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_FAILED); this->status_set_error(); // Also remove from loop since failed components shouldn't loop @@ -213,14 +210,14 @@ void Component::set_component_state_(uint8_t state) { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP_DONE); App.disable_component_loop_(this); } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } @@ -240,7 +237,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); + ESP_LOGI(TAG, "%s is being reset to construction state", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_CONSTRUCTION); // Clear error status when resetting this->status_clear_error(); @@ -280,20 +277,32 @@ bool Component::is_ready() const { bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } + void Component::status_set_warning(const char *message) { // Don't spam the log. This risks missing different warning messages though. if ((this->component_state_ & STATUS_LED_WARNING) != 0) return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? message : LOG_STR_LITERAL("unspecified")); +} +void Component::status_set_warning(const LogString *message) { + // Don't spam the log. This risks missing different warning messages though. + if ((this->component_state_ & STATUS_LED_WARNING) != 0) + return; + this->component_state_ |= STATUS_LED_WARNING; + App.app_state_ |= STATUS_LED_WARNING; + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { @@ -314,13 +323,13 @@ void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; this->component_state_ &= ~STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); + ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_clear_error() { if ((this->component_state_ & STATUS_LED_ERROR) == 0) return; this->component_state_ &= ~STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); + ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); @@ -331,6 +340,18 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } void Component::dump_config() {} + +// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size +void log_update_interval(const char *tag, PollingComponent *component) { + uint32_t update_interval = component->get_update_interval(); + if (update_interval == SCHEDULER_DONT_RUN) { + ESP_LOGCONFIG(tag, " Update Interval: never"); + } else if (update_interval < 100) { + ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f); + } else { + ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f); + } +} float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector if (setup_priority_overrides) { @@ -419,8 +440,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() { should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; } if (should_warn) { - const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", + component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), + blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms"); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 096c6f9c69..e97941374d 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,10 +5,14 @@ #include #include +#include "esphome/core/log.h" #include "esphome/core/optional.h" namespace esphome { +// Forward declaration for LogString +struct LogString; + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -44,14 +48,13 @@ extern const float LATE; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; -#define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ - ESP_LOGCONFIG(TAG, " Update Interval: never"); \ - } else if (this->get_update_interval() < 100) { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ - } else { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ - } +// Forward declaration +class PollingComponent; + +// Function declaration for LOG_UPDATE_INTERVAL +void log_update_interval(const char *tag, PollingComponent *component); + +#define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) extern const uint8_t COMPONENT_STATE_MASK; extern const uint8_t COMPONENT_STATE_CONSTRUCTION; @@ -203,6 +206,7 @@ class Component { bool status_has_error() const; void status_set_warning(const char *message = nullptr); + void status_set_warning(const LogString *message); void status_set_error(const char *message = nullptr); @@ -220,12 +224,12 @@ class Component { * * This is set by the ESPHome core, and should not be called manually. */ - void set_component_source(const char *source) { component_source_ = source; } - /** Get the integration where this component was declared as a string. + void set_component_source(const LogString *source) { component_source_ = source; } + /** Get the integration where this component was declared as a LogString for logging. * - * Returns "" if source not set + * Returns LOG_STR("") if source not set */ - const char *get_component_source() const; + const LogString *get_component_log_str() const; bool should_warn_of_blocking(uint32_t blocking_time); @@ -405,7 +409,7 @@ class Component { bool cancel_defer(const std::string &name); // NOLINT // Ordered for optimal packing on 32-bit systems - const char *component_source_{nullptr}; + const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index b77a02b2a7..6a2232599f 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -78,9 +78,13 @@ size_t RingBuffer::write(const void *data, size_t len) { 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) { +size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait, + bool write_partial) { if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) { - // Couldn't fit all the data, so only write what will fit + if (!write_partial) { + return 0; // Not enough space available and not allowed to write partial data + } + // Couldn't fit all the data, write what will fit size_t free = std::min(this->free(), len); if (xRingbufferSend(this->handle_, data, free, 0)) { return free; diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index bad96d3181..98a273781f 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -50,7 +50,8 @@ class RingBuffer { * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) * @return Number of bytes written */ - size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0); + size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0, + bool write_partial = true); /** * @brief Returns the number of available bytes in the ring buffer. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a907b89b02..68da0a56ca 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,7 +14,19 @@ namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; +// Memory pool configuration constants +// Pool size of 5 matches typical usage patterns (2-4 active timers) +// - Minimal memory overhead (~250 bytes on ESP32) +// - Sufficient for most configs with a couple sensors/components +// - Still prevents heap fragmentation and allocation stalls +// - Complex setups with many timers will just allocate beyond the pool +// See https://github.com/esphome/backlog/issues/52 +static constexpr size_t MAX_POOL_SIZE = 5; + +// Maximum number of logically deleted (cancelled) items before forcing cleanup. +// Set to 5 to match the pool size - when we have as many cancelled items as our +// pool can hold, it's time to clean up and recycle them. +static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; // max delay to start an interval sequence @@ -79,8 +91,28 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } + // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself + const uint64_t now = this->millis_64_(millis()); + + // Take lock early to protect scheduler_item_pool_ access + LockGuard guard{this->lock_}; + // Create and populate the scheduler item - auto item = make_unique(); + std::unique_ptr item; + if (!this->scheduler_item_pool_.empty()) { + // Reuse from pool + item = std::move(this->scheduler_item_pool_.back()); + this->scheduler_item_pool_.pop_back(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { + // Allocate new if pool is empty + item = make_unique(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + } item->component = component; item->set_name(name_cstr, !is_static_string); item->type = type; @@ -99,7 +131,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - LockGuard guard{this->lock_}; if (!skip_cancel) { this->cancel_item_locked_(component, name_cstr, type); } @@ -108,21 +139,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif /* not ESPHOME_THREAD_SINGLE */ - // Get fresh timestamp for new timer/interval - ensures accurate scheduling - const auto now = this->millis_64_(millis()); // Fresh millis() call - // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; // first execution happens immediately after a random smallish offset // Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->next_execution_ = now + offset; + item->set_next_execution(now + offset); ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, offset); } else { item->interval = 0; - item->next_execution_ = now + delay; + item->set_next_execution(now + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -134,16 +162,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Debug logging const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), name_cstr ? name_cstr : "(null)", type_str, delay); } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), - name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); } #endif /* ESPHOME_DEBUG_SCHEDULER */ - LockGuard guard{this->lock_}; - // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || @@ -285,9 +312,10 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { auto &item = this->items_[0]; // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller - if (item->next_execution_ < now_64) + const uint64_t next_exec = item->get_next_execution(); + if (next_exec < now_64) return 0; - return item->next_execution_ - now_64; + return next_exec - now_64; } void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE @@ -319,6 +347,8 @@ void HOT Scheduler::call(uint32_t now) { if (!this->should_skip_item_(item.get())) { this->execute_item_(item.get(), now); } + // Recycle the defer item after execution + this->recycle_item_(std::move(item)); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -326,6 +356,9 @@ void HOT Scheduler::call(uint32_t now) { const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() this->process_to_add(); + // Track if any items were added to to_add_ during this call (intervals or from callbacks) + bool has_added_items = false; + #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; @@ -335,11 +368,11 @@ void HOT Scheduler::call(uint32_t now) { #ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - major_dbg, last_dbg); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - this->millis_major_, this->last_millis_); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ // Cleanup before debug output this->cleanup_(); @@ -352,9 +385,10 @@ void HOT Scheduler::call(uint32_t now) { } const char *name = item->get_name(); - ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, - item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now_64, item->next_execution_); + bool is_cancelled = is_item_removed_(item.get()); + ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", + item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval, + item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); } @@ -369,8 +403,13 @@ void HOT Scheduler::call(uint32_t now) { } #endif /* ESPHOME_DEBUG_SCHEDULER */ - // If we have too many items to remove - if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + // Cleanup removed items before processing + // First try to clean items from the top of the heap (fast path) + this->cleanup_(); + + // If we still have too many cancelled items, do a full cleanup + // This only happens if cancelled items are stuck in the middle/bottom of the heap + if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { // We hold the lock for the entire cleanup operation because: // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout // 2. Other threads must see either the old state or the new state, not intermediate states @@ -380,10 +419,13 @@ void HOT Scheduler::call(uint32_t now) { std::vector> valid_items; - // Move all non-removed items to valid_items + // Move all non-removed items to valid_items, recycle removed ones for (auto &item : this->items_) { - if (!item->remove) { + if (!is_item_removed_(item.get())) { valid_items.push_back(std::move(item)); + } else { + // Recycle removed items + this->recycle_item_(std::move(item)); } } @@ -393,92 +435,92 @@ void HOT Scheduler::call(uint32_t now) { std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->to_remove_ = 0; } - - // Cleanup removed items before processing - this->cleanup_(); while (!this->items_.empty()) { - // use scoping to indicate visibility of `item` variable - { - // Don't copy-by value yet - auto &item = this->items_[0]; - if (item->next_execution_ > now_64) { - // Not reached timeout yet, done for this call - break; - } - // Don't run on failed components - if (item->component != nullptr && item->component->is_failed()) { - LockGuard guard{this->lock_}; - this->pop_raw_(); - continue; - } + // Don't copy-by value yet + auto &item = this->items_[0]; + if (item->get_next_execution() > now_64) { + // Not reached timeout yet, done for this call + break; + } + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + continue; + } - // Check if item is marked for removal - // This handles two cases: - // 1. Item was marked for removal after cleanup_() but before we got here - // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() + // Check if item is marked for removal + // This handles two cases: + // 1. Item was marked for removal after cleanup_() but before we got here + // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() #ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS - // Multi-threaded platforms without atomics: must take lock to safely read remove flag - { - LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { - this->pop_raw_(); - this->to_remove_--; - continue; - } - } -#else - // Single-threaded or multi-threaded with atomics: can check without lock + // Multi-threaded platforms without atomics: must take lock to safely read remove flag + { + LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - LockGuard guard{this->lock_}; this->pop_raw_(); this->to_remove_--; continue; } + } +#else + // Single-threaded or multi-threaded with atomics: can check without lock + if (is_item_removed_(item.get())) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + this->to_remove_--; + continue; + } #endif #ifdef ESPHOME_DEBUG_SCHEDULER - const char *item_name = item->get_name(); - ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, - item->next_execution_, now_64); + const char *item_name = item->get_name(); + ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", + item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, + item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // Warning: During callback(), a lot of stuff can happen, including: - // - timeouts/intervals get added, potentially invalidating vector pointers - // - timeouts/intervals get cancelled - this->execute_item_(item.get(), now); + // Warning: During callback(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + this->execute_item_(item.get(), now); + + LockGuard guard{this->lock_}; + + auto executed_item = std::move(this->items_[0]); + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (executed_item->remove) { + // We were removed/cancelled in the function call, stop + this->to_remove_--; + continue; } - { - LockGuard guard{this->lock_}; - - // new scope, item from before might have been moved in the vector - auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable - // during the function call and know if we were cancelled. - this->pop_raw_(); - - if (item->remove) { - // We were removed/cancelled in the function call, stop - this->to_remove_--; - continue; - } - - if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now_64 + item->interval; - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); - } + if (executed_item->type == SchedulerItem::INTERVAL) { + executed_item->set_next_execution(now_64 + executed_item->interval); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(executed_item)); + } else { + // Timeout completed - recycle it + this->recycle_item_(std::move(executed_item)); } + + has_added_items |= !this->to_add_.empty(); } - this->process_to_add(); + if (has_added_items) { + this->process_to_add(); + } } void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (it->remove) { + if (is_item_removed_(it.get())) { + // Recycle cancelled items + this->recycle_item_(std::move(it)); continue; } @@ -518,6 +560,10 @@ size_t HOT Scheduler::cleanup_() { } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + + // Instead of destroying, recycle the item + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); } @@ -552,7 +598,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Check all containers for matching items #ifndef ESPHOME_THREAD_SINGLE - // Only check defer queue for timeouts (intervals never go there) + // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) { @@ -564,11 +610,22 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); + // Special case: if the last item in the heap matches, we can remove it immediately + // (removing the last element doesn't break heap structure) + if (!this->items_.empty()) { + auto &last_item = this->items_.back(); + if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); total_cancelled++; - this->to_remove_++; // Track removals for heap items + } + // For other items in heap, we can only mark for removal (can't remove from middle of heap) + for (auto &item : this->items_) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + this->mark_item_removed_(item.get()); + total_cancelled++; + this->to_remove_++; // Track removals for heap items + } } } @@ -744,7 +801,31 @@ uint64_t Scheduler::millis_64_(uint32_t now) { bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, const std::unique_ptr &b) { - return a->next_execution_ > b->next_execution_; + // High bits are almost always equal (change only on 32-bit rollover ~49 days) + // Optimize for common case: check low bits first when high bits are equal + return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) + : (a->next_execution_high_ > b->next_execution_high_); +} + +void Scheduler::recycle_item_(std::unique_ptr item) { + if (!item) + return; + + if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { + // Clear callback to release captured resources + item->callback = nullptr; + // Clear dynamic name if any + item->clear_dynamic_name(); + this->scheduler_item_pool_.push_back(std::move(item)); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); +#endif + } + // else: unique_ptr will delete the item when it goes out of scope } } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index f469a60d5c..301342e8c2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -88,19 +88,22 @@ class Scheduler { struct SchedulerItem { // Ordered by size to minimize padding Component *component; - uint32_t interval; - // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 64-bit time that won't roll over for - // billions of years. This ensures correct scheduling even when devices run for months. - uint64_t next_execution_; - // Optimized name storage using tagged union union { const char *static_name; // For string literals (no allocation) char *dynamic_name; // For allocated strings } name_; - + uint32_t interval; + // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). + // This is intentionally limited to 48 bits, not stored as a full 64-bit value. + // With 49.7 days per 32-bit rollover, the 16-bit counter supports + // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // even when devices run for months. Split into two fields for better memory + // alignment on 32-bit systems. + uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) std::function callback; + uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS // Multi-threaded with atomics: use atomic for lock-free access @@ -126,7 +129,8 @@ class Scheduler { SchedulerItem() : component(nullptr), interval(0), - next_execution_(0), + next_execution_low_(0), + next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS // remove is initialized in the member declaration as std::atomic{false} type(TIMEOUT), @@ -142,11 +146,7 @@ class Scheduler { } // Destructor to clean up dynamic names - ~SchedulerItem() { - if (name_is_dynamic) { - delete[] name_.dynamic_name; - } - } + ~SchedulerItem() { clear_dynamic_name(); } // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; @@ -159,13 +159,19 @@ class Scheduler { // Helper to get the name regardless of storage type const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + // Helper to clear dynamic name if allocated + void clear_dynamic_name() { + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_.dynamic_name = nullptr; + name_is_dynamic = false; + } + } + // Helper to set name with proper ownership void set_name(const char *name, bool make_copy = false) { // Clean up old dynamic name if any - if (name_is_dynamic && name_.dynamic_name) { - delete[] name_.dynamic_name; - name_is_dynamic = false; - } + clear_dynamic_name(); if (!name) { // nullptr case - no name provided @@ -183,8 +189,22 @@ class Scheduler { } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } - const char *get_source() const { return component ? component->get_component_source() : "unknown"; } + + // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. + // The upper 16 bits of the 64-bit value are always zero, which is fine since + // millis_major_ is also 16 bits and they must match. + constexpr uint64_t get_next_execution() const { + return (static_cast(next_execution_high_) << 32) | next_execution_low_; + } + + constexpr void set_next_execution(uint64_t value) { + next_execution_low_ = static_cast(value); + // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits. + // This is correct because millis_major_ that creates these values is also 16 bits. + next_execution_high_ = static_cast(value >> 32); + } + constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); } }; // Common implementation for both timeout and interval @@ -214,6 +234,15 @@ class Scheduler { // Common implementation for cancel operations bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + // Helper to check if two scheduler item names match + inline bool HOT names_match_(const char *name1, const char *name2) const { + // Check pointer equality first (common for static strings), then string contents + // The core ESPHome codebase uses static strings (const char*) for component names, + // making pointer comparison effective. The std::string overloads exist only for + // compatibility with external components but are rarely used in practice. + return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0)); + } + // Helper function to check if item matches criteria for cancellation inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { @@ -221,29 +250,20 @@ class Scheduler { (match_retry && !item->is_retry)) { return false; } - const char *item_name = item->get_name(); - if (item_name == nullptr) { - return false; - } - // Fast path: if pointers are equal - // This is effective because the core ESPHome codebase uses static strings (const char*) - // for component names. The std::string overloads exist only for compatibility with - // external components, but are rarely used in practice. - if (item_name == name_cstr) { - return true; - } - // Slow path: compare string contents - return strcmp(name_cstr, item_name) == 0; + return this->names_match_(item->get_name(), name_cstr); } // Helper to execute a scheduler item void execute_item_(SchedulerItem *item, uint32_t now); // Helper to check if item should be skipped - bool should_skip_item_(const SchedulerItem *item) const { - return item->remove || (item->component != nullptr && item->component->is_failed()); + bool should_skip_item_(SchedulerItem *item) const { + return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed()); } + // Helper to recycle a SchedulerItem + void recycle_item_(std::unique_ptr item); + // Helper to check if item is marked for removal (platform-specific) // Returns true if item should be skipped, handles platform-specific synchronization // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this @@ -280,8 +300,9 @@ class Scheduler { bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, bool match_retry) const { for (const auto &item : container) { - if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + if (is_item_removed_(item.get()) && + this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } @@ -297,6 +318,16 @@ class Scheduler { #endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; + // Memory pool for recycling SchedulerItem objects to reduce heap churn. + // Design decisions: + // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items + // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups + // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32) + // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation + // can stall the entire system, causing timing issues and dropped events for any components that need + // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) + std::vector> scheduler_item_pool_; + #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 34e4eec1ee..291592dd2b 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -253,6 +253,19 @@ class StringLiteral(Literal): return cpp_string_escape(self.string) +class LogStringLiteral(Literal): + """A string literal that uses LOG_STR() macro for flash storage on ESP8266.""" + + __slots__ = ("string",) + + def __init__(self, string: str) -> None: + super().__init__() + self.string = string + + def __str__(self) -> str: + return f"LOG_STR({cpp_string_escape(self.string)})" + + class IntLiteral(Literal): __slots__ = ("i",) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b61b215bdc..2698b9b3d5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -76,7 +76,7 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(name)) + add(var.set_component_source(LogStringLiteral(name))) add(App.register_component(var)) return var diff --git a/esphome/espota2.py b/esphome/espota2.py index 279bafee8e..3d25af985b 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -308,8 +308,12 @@ def perform_ota( time.sleep(1) -def run_ota_impl_(remote_host, remote_port, password, filename): +def run_ota_impl_( + remote_host: str | list[str], remote_port: int, password: str, filename: str +) -> tuple[int, str | None]: + # Handle both single host and list of hosts try: + # Resolve all hosts at once for parallel DNS resolution res = resolve_ip_address(remote_host, remote_port) except EsphomeError as err: _LOGGER.error( @@ -340,19 +344,22 @@ def run_ota_impl_(remote_host, remote_port, password, filename): perform_ota(sock, password, file_handle, filename) except OTAError as err: _LOGGER.error(str(err)) - return 1 + return 1, None finally: sock.close() - return 0 + # Successfully uploaded to sa[0] + return 0, sa[0] _LOGGER.error("Connection failed.") - return 1 + return 1, None -def run_ota(remote_host, remote_port, password, filename): +def run_ota( + remote_host: str | list[str], remote_port: int, password: str, filename: str +) -> tuple[int, str | None]: try: return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) - return 1 + return 1, None diff --git a/esphome/helpers.py b/esphome/helpers.py index 377a4e1717..6beaa24a96 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import codecs from contextlib import suppress import ipaddress @@ -11,6 +13,18 @@ from urllib.parse import urlparse from esphome.const import __version__ as ESPHOME_VERSION +# Type aliases for socket address information +AddrInfo = tuple[ + int, # family (AF_INET, AF_INET6, etc.) + int, # type (SOCK_STREAM, SOCK_DGRAM, etc.) + int, # proto (IPPROTO_TCP, etc.) + str, # canonname + tuple[str, int] | tuple[str, int, int, int], # sockaddr (IPv4 or IPv6) +] +IPv4SockAddr = tuple[str, int] # (host, port) +IPv6SockAddr = tuple[str, int, int, int] # (host, port, flowinfo, scope_id) +SockAddr = IPv4SockAddr | IPv6SockAddr + _LOGGER = logging.getLogger(__name__) IS_MACOS = platform.system() == "Darwin" @@ -147,32 +161,7 @@ def is_ip_address(host): return False -def _resolve_with_zeroconf(host): - from esphome.core import EsphomeError - from esphome.zeroconf import EsphomeZeroconf - - try: - zc = EsphomeZeroconf() - except Exception as err: - raise EsphomeError( - "Cannot start mDNS sockets, is this a docker container without " - "host network mode?" - ) from err - try: - info = zc.resolve_host(f"{host}.") - except Exception as err: - raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err - finally: - zc.close() - if info is None: - raise EsphomeError( - "Error resolving address with mDNS: Did not respond. " - "Maybe the device is offline." - ) - return info - - -def addr_preference_(res): +def addr_preference_(res: AddrInfo) -> int: # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then # Legacy IP, then IPv6 link-local addresses without an actual link. sa = res[4] @@ -184,66 +173,70 @@ def addr_preference_(res): return 1 -def resolve_ip_address(host, port): +def resolve_ip_address(host: str | list[str], port: int) -> list[AddrInfo]: import socket - from esphome.core import EsphomeError - # There are five cases here. The host argument could be one of: # • a *list* of IP addresses discovered by MQTT, # • a single IP address specified by the user, # • a .local hostname to be resolved by mDNS, # • a normal hostname to be resolved in DNS, or # • A URL from which we should extract the hostname. - # - # In each of the first three cases, we end up with IP addresses in - # string form which need to be converted to a 5-tuple to be used - # for the socket connection attempt. The easiest way to construct - # those is to pass the IP address string to getaddrinfo(). Which, - # coincidentally, is how we do hostname lookups in the other cases - # too. So first build a list which contains either IP addresses or - # a single hostname, then call getaddrinfo() on each element of - # that list. - errs = [] + hosts: list[str] if isinstance(host, list): - addr_list = host - elif is_ip_address(host): - addr_list = [host] + hosts = host else: - url = urlparse(host) - if url.scheme != "": - host = url.hostname + if not is_ip_address(host): + url = urlparse(host) + if url.scheme != "": + host = url.hostname + hosts = [host] - addr_list = [] - if host.endswith(".local"): + res: list[AddrInfo] = [] + if all(is_ip_address(h) for h in hosts): + # Fast path: all are IP addresses, use socket.getaddrinfo with AI_NUMERICHOST + for addr in hosts: try: - _LOGGER.info("Resolving IP address of %s in mDNS", host) - addr_list = _resolve_with_zeroconf(host) - except EsphomeError as err: - errs.append(str(err)) + res += socket.getaddrinfo( + addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + except OSError: + _LOGGER.debug("Failed to parse IP address '%s'", addr) + # Sort by preference + res.sort(key=addr_preference_) + return res - # If not mDNS, or if mDNS failed, use normal DNS - if not addr_list: - addr_list = [host] + from esphome.resolver import AsyncResolver - # Now we have a list containing either IP addresses or a hostname - res = [] - for addr in addr_list: - if not is_ip_address(addr): - _LOGGER.info("Resolving IP address of %s", host) - try: - r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP) - except OSError as err: - errs.append(str(err)) - raise EsphomeError( - f"Error resolving IP address: {', '.join(errs)}" - ) from err + resolver = AsyncResolver(hosts, port) + addr_infos = resolver.resolve() + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) - res = res + r + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) + ) - # Zeroconf tends to give us link-local IPv6 addresses without specifying - # the link. Put those last in the list to be attempted. + # Sort by preference res.sort(key=addr_preference_) return res @@ -262,15 +255,7 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]: # First "resolve" all the IP addresses to getaddrinfo() tuples of the form # (family, type, proto, canonname, sockaddr) - res: list[ - tuple[ - int, - int, - int, - str | None, - tuple[str, int] | tuple[str, int, int, int], - ] - ] = [] + res: list[AddrInfo] = [] for addr in address_list: # This should always work as these are supposed to be IP addresses try: diff --git a/esphome/resolver.py b/esphome/resolver.py new file mode 100644 index 0000000000..99482aa20e --- /dev/null +++ b/esphome/resolver.py @@ -0,0 +1,67 @@ +"""DNS resolver for ESPHome using aioesphomeapi.""" + +from __future__ import annotations + +import asyncio +import threading + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +import aioesphomeapi.host_resolver as hr + +from esphome.core import EsphomeError + +RESOLVE_TIMEOUT = 10.0 # seconds + + +class AsyncResolver(threading.Thread): + """Resolver using aioesphomeapi that runs in a thread for faster results. + + This resolver uses aioesphomeapi's async_resolve_host to handle DNS resolution, + including proper .local domain fallback. Running in a thread allows us to get + the result immediately without waiting for asyncio.run() to complete its + cleanup cycle, which can take significant time. + """ + + def __init__(self, hosts: list[str], port: int) -> None: + """Initialize the resolver.""" + super().__init__(daemon=True) + self.hosts = hosts + self.port = port + self.result: list[hr.AddrInfo] | None = None + self.exception: Exception | None = None + self.event = threading.Event() + + async def _resolve(self) -> None: + """Resolve hostnames to IP addresses.""" + try: + self.result = await hr.async_resolve_host( + self.hosts, self.port, timeout=RESOLVE_TIMEOUT + ) + except Exception as e: # pylint: disable=broad-except + # We need to catch all exceptions to ensure the event is set + # Otherwise the thread could hang forever + self.exception = e + finally: + self.event.set() + + def run(self) -> None: + """Run the DNS resolution.""" + asyncio.run(self._resolve()) + + def resolve(self) -> list[hr.AddrInfo]: + """Start the thread and wait for the result.""" + self.start() + + if not self.event.wait( + timeout=RESOLVE_TIMEOUT + 1.0 + ): # Give it 1 second more than the resolver timeout + raise EsphomeError("Timeout resolving IP address") + + if exc := self.exception: + if isinstance(exc, ResolveTimeoutAPIError): + raise EsphomeError(f"Timeout resolving IP address: {exc}") from exc + if isinstance(exc, ResolveAPIError): + raise EsphomeError(f"Error resolving IP address: {exc}") from exc + raise exc + + return self.result diff --git a/requirements.txt b/requirements.txt index 32fdfabcda..0b9a37005d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,9 +11,9 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 -esphome-dashboard==20250828.0 -aioesphomeapi==39.0.1 -zeroconf==0.147.0 +esphome-dashboard==20250904.0 +aioesphomeapi==40.1.0 +zeroconf==0.147.2 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import esphome-glyphsets==0.2.0 diff --git a/requirements_test.txt b/requirements_test.txt index 22f58fd3d7..bae9246768 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,13 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.11 # also change in .pre-commit-config.yaml when updating +ruff==0.12.12 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 +pytest==8.4.2 +pytest-cov==7.0.0 +pytest-mock==3.15.0 pytest-asyncio==1.1.0 pytest-xdist==3.8.0 asyncmock==0.4.2 diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 4581620095..27ea82611b 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -82,7 +82,7 @@ for path in components_dir.iterdir(): for path, owners in sorted(codeowners.items()): - owners = sorted(set(owners)) + owners = sorted(set(owners), key=str.casefold) if not owners: continue for owner in owners: diff --git a/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml b/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml b/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.nrf52-adafruit.yaml b/tests/components/binary_sensor/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.nrf52-mcumgr.yaml b/tests/components/binary_sensor/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.nrf52-adafruit.yaml b/tests/components/button/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.nrf52-mcumgr.yaml b/tests/components/button/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.nrf52-adafruit.yaml b/tests/components/duty_cycle/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.nrf52-mcumgr.yaml b/tests/components/duty_cycle/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.nrf52-adafruit.yaml b/tests/components/duty_time/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.nrf52-mcumgr.yaml b/tests/components/duty_time/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esphome/test.nrf52-adafruit.yaml b/tests/components/esphome/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esphome/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esphome/test.nrf52-mcumgr.yaml b/tests/components/esphome/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esphome/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.nrf52-adafruit.yaml b/tests/components/event/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.nrf52-mcumgr.yaml b/tests/components/event/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.nrf52-adafruit.yaml b/tests/components/interval/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.nrf52-mcumgr.yaml b/tests/components/interval/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/light/test.nrf52-adafruit.yaml b/tests/components/light/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-adafruit.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/light/test.nrf52-mcumgr.yaml b/tests/components/light/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-mcumgr.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/lock/test.nrf52-adafruit.yaml b/tests/components/lock/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.nrf52-mcumgr.yaml b/tests/components/lock/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8d0e20d6f5 --- /dev/null +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -0,0 +1,67 @@ +psram: + mode: octal + +spi: + - clk_pin: + number: 47 + allow_other_uses: true + mosi_pin: + number: 41 + allow_other_uses: true + +display: + - platform: mipi_rgb + model: ZX2D10GE01R-V4848 + update_interval: 1s + color_order: BGR + draw_rounding: 2 + pixel_mode: 18bit + invert_colors: false + use_axis_flips: true + pclk_frequency: 15000000.0 + pclk_inverted: true + byte_order: big_endian + hsync_pulse_width: 10 + hsync_back_porch: 10 + hsync_front_porch: 10 + vsync_pulse_width: 2 + vsync_back_porch: 12 + vsync_front_porch: 14 + data_pins: + red: + - number: 10 + - number: 16 + - number: 9 + - number: 15 + - number: 46 + ignore_strapping_warning: true + green: + - number: 8 + - number: 13 + - number: 18 + - number: 12 + - number: 11 + - number: 17 + blue: + - number: 47 + allow_other_uses: true + - number: 41 + allow_other_uses: true + - number: 0 + ignore_strapping_warning: true + - number: 42 + - number: 14 + de_pin: + number: 39 + pclk_pin: + number: 45 + ignore_strapping_warning: true + hsync_pin: + number: 40 + vsync_pin: + number: 48 + data_rate: 1000000.0 + spi_mode: MODE0 + cs_pin: + number: 21 + show_test_card: true diff --git a/tests/components/power_supply/test.nrf52-adafruit.yaml b/tests/components/power_supply/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.nrf52-mcumgr.yaml b/tests/components/power_supply/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.nrf52-adafruit.yaml b/tests/components/pulse_counter/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.nrf52-mcumgr.yaml b/tests/components/pulse_counter/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.nrf52-adafruit.yaml b/tests/components/pulse_meter/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.nrf52-mcumgr.yaml b/tests/components/pulse_meter/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.nrf52-adafruit.yaml b/tests/components/pulse_width/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.nrf52-mcumgr.yaml b/tests/components/pulse_width/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.nrf52-adafruit.yaml b/tests/components/switch/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/switch/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.nrf52-mcumgr.yaml b/tests/components/switch/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/switch/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.nrf52-adafruit.yaml b/tests/components/version/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.nrf52-mcumgr.yaml b/tests/components/version/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp index 7e88950592..6e128687c4 100644 --- a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp @@ -27,11 +27,13 @@ void GPIOExpanderTestComponent::setup() { bool GPIOExpanderTestComponent::digital_read_hw(uint8_t pin) { ESP_LOGD(TAG, "digital_read_hw pin=%d", pin); + // Return true to indicate successful read operation return true; } bool GPIOExpanderTestComponent::digital_read_cache(uint8_t pin) { ESP_LOGD(TAG, "digital_read_cache pin=%d", pin); + // Return the pin state (always HIGH for testing) return true; } diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py new file mode 100644 index 0000000000..76f20b942c --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +AUTO_LOAD = ["gpio_expander"] + +gpio_expander_test_component_uint16_ns = cg.esphome_ns.namespace( + "gpio_expander_test_component_uint16" +) + +GPIOExpanderTestUint16Component = gpio_expander_test_component_uint16_ns.class_( + "GPIOExpanderTestUint16Component", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOExpanderTestUint16Component), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp new file mode 100644 index 0000000000..09537c81bb --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp @@ -0,0 +1,43 @@ +#include "gpio_expander_test_component_uint16.h" +#include "esphome/core/log.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +static const char *const TAG = "gpio_expander_test_uint16"; + +void GPIOExpanderTestUint16Component::setup() { + ESP_LOGD(TAG, "Testing uint16_t bank (single 16-pin bank)"); + + // Test reading all 16 pins - first should trigger hw read, rest use cache + for (uint8_t pin = 0; pin < 16; pin++) { + this->digital_read(pin); + } + + // Reset cache and test specific reads + ESP_LOGD(TAG, "Resetting cache for uint16_t test"); + this->reset_pin_cache_(); + + // First read triggers hw for entire bank + this->digital_read(5); + // These should all use cache since they're in the same bank + this->digital_read(10); + this->digital_read(15); + this->digital_read(0); + + ESP_LOGD(TAG, "DONE_UINT16"); +} + +bool GPIOExpanderTestUint16Component::digital_read_hw(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_hw pin=%d", pin); + // In a real component, this would read from I2C/SPI into internal state + // For testing, we just return true to indicate successful read + return true; // Return true to indicate successful read +} + +bool GPIOExpanderTestUint16Component::digital_read_cache(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_cache pin=%d", pin); + // Return the actual pin state from our test pattern + return (this->test_state_ >> pin) & 1; +} + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h new file mode 100644 index 0000000000..be102f9b57 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/core/component.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +// Test component using uint16_t bank type (single 16-pin bank) +class GPIOExpanderTestUint16Component : public Component, + public esphome::gpio_expander::CachedGpioExpander { + public: + void setup() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override{}; + + private: + uint16_t test_state_{0xAAAA}; // Test pattern: alternating bits +}; + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/gpio_expander_cache.yaml b/tests/integration/fixtures/gpio_expander_cache.yaml index 7d7ca1a876..8b5375af4c 100644 --- a/tests/integration/fixtures/gpio_expander_cache.yaml +++ b/tests/integration/fixtures/gpio_expander_cache.yaml @@ -12,6 +12,10 @@ external_components: - source: type: local path: EXTERNAL_COMPONENT_PATH - components: [gpio_expander_test_component] + components: [gpio_expander_test_component, gpio_expander_test_component_uint16] +# Test with uint8_t (multiple banks) gpio_expander_test_component: + +# Test with uint16_t (single bank) +gpio_expander_test_component_uint16: diff --git a/tests/integration/fixtures/scheduler_pool.yaml b/tests/integration/fixtures/scheduler_pool.yaml new file mode 100644 index 0000000000..5389125188 --- /dev/null +++ b/tests/integration/fixtures/scheduler_pool.yaml @@ -0,0 +1,282 @@ +esphome: + name: scheduler-pool-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler pool tests" + debug_scheduler: true # Enable scheduler debug logging + +host: +api: + services: + - service: run_phase_1 + then: + - script.execute: test_pool_recycling + - service: run_phase_2 + then: + - script.execute: test_sensor_polling + - service: run_phase_3 + then: + - script.execute: test_communication_patterns + - service: run_phase_4 + then: + - script.execute: test_defer_patterns + - service: run_phase_5 + then: + - script.execute: test_pool_reuse_verification + - service: run_phase_6 + then: + - script.execute: test_full_pool_reuse + - service: run_phase_7 + then: + - script.execute: test_same_defer_optimization + - service: run_complete + then: + - script.execute: complete_test +logger: + level: VERY_VERBOSE # Need VERY_VERBOSE to see pool debug messages + +globals: + - id: create_count + type: int + initial_value: '0' + - id: cancel_count + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: pool_test_done + type: bool + initial_value: 'false' + +script: + - id: test_pool_recycling + then: + - logger.log: "Testing scheduler pool recycling with realistic usage patterns" + - lambda: |- + auto *component = id(test_sensor); + + // Simulate realistic component behavior with timeouts that complete naturally + ESP_LOGI("test", "Phase 1: Simulating normal component lifecycle"); + + // Sensor update timeouts (common pattern) + App.scheduler.set_timeout(component, "sensor_init", 10, []() { + ESP_LOGD("test", "Sensor initialized"); + id(create_count)++; + }); + + // Retry timeout (gets cancelled if successful) + App.scheduler.set_timeout(component, "retry_timeout", 50, []() { + ESP_LOGD("test", "Retry timeout executed"); + id(create_count)++; + }); + + // Simulate successful operation - cancel retry + App.scheduler.set_timeout(component, "success_sim", 20, []() { + ESP_LOGD("test", "Operation succeeded, cancelling retry"); + App.scheduler.cancel_timeout(id(test_sensor), "retry_timeout"); + id(cancel_count)++; + }); + + id(create_count) += 3; + ESP_LOGI("test", "Phase 1 complete"); + + - id: test_sensor_polling + then: + - lambda: |- + // Simulate sensor polling pattern + ESP_LOGI("test", "Phase 2: Simulating sensor polling patterns"); + auto *component = id(test_sensor); + + // Multiple sensors with different update intervals + // These should only allocate once and reuse the same item for each interval execution + App.scheduler.set_interval(component, "temp_sensor", 10, []() { + ESP_LOGD("test", "Temperature sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor), "temp_sensor"); + ESP_LOGD("test", "Temperature sensor stopped"); + } + }); + + App.scheduler.set_interval(component, "humidity_sensor", 15, []() { + ESP_LOGD("test", "Humidity sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 5) { + App.scheduler.cancel_interval(id(test_sensor), "humidity_sensor"); + ESP_LOGD("test", "Humidity sensor stopped"); + } + }); + + // Only 2 allocations for the intervals, no matter how many times they execute + id(create_count) += 2; + ESP_LOGD("test", "Created 2 intervals - they will reuse same items for each execution"); + ESP_LOGI("test", "Phase 2 complete"); + + - id: test_communication_patterns + then: + - lambda: |- + // Simulate communication patterns (WiFi/API reconnects, etc) + ESP_LOGI("test", "Phase 3: Simulating communication patterns"); + auto *component = id(test_sensor); + + // Connection timeout pattern + App.scheduler.set_timeout(component, "connect_timeout", 200, []() { + ESP_LOGD("test", "Connection timeout - would retry"); + id(create_count)++; + + // Schedule retry + App.scheduler.set_timeout(id(test_sensor), "connect_retry", 100, []() { + ESP_LOGD("test", "Retrying connection"); + id(create_count)++; + }); + }); + + // Heartbeat pattern + App.scheduler.set_interval(component, "heartbeat", 50, []() { + ESP_LOGD("test", "Heartbeat"); + id(interval_counter)++; + if (id(interval_counter) >= 10) { + App.scheduler.cancel_interval(id(test_sensor), "heartbeat"); + ESP_LOGD("test", "Heartbeat stopped"); + } + }); + + id(create_count) += 2; + ESP_LOGI("test", "Phase 3 complete"); + + - id: test_defer_patterns + then: + - lambda: |- + // Simulate defer patterns (state changes, async operations) + ESP_LOGI("test", "Phase 4: Simulating heavy defer patterns like ratgdo"); + + auto *component = id(test_sensor); + + // Simulate a burst of defer operations like ratgdo does with state updates + // These should execute immediately and recycle quickly to the pool + for (int i = 0; i < 10; i++) { + std::string defer_name = "defer_" + std::to_string(i); + App.scheduler.set_timeout(component, defer_name, 0, [i]() { + ESP_LOGD("test", "Defer %d executed", i); + // Force a small delay between defer executions to see recycling + if (i == 5) { + ESP_LOGI("test", "Half of defers executed, checking pool status"); + } + }); + } + + id(create_count) += 10; + ESP_LOGD("test", "Created 10 defer operations (0ms timeouts)"); + + // Also create some named defers that might get replaced + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 1"); + }); + + // Replace the same named defer (should cancel previous) + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 2 (replaced)"); + }); + + id(create_count) += 2; + id(cancel_count) += 1; // One cancelled due to replacement + + ESP_LOGI("test", "Phase 4 complete"); + + - id: test_pool_reuse_verification + then: + - lambda: |- + ESP_LOGI("test", "Phase 5: Verifying pool reuse after everything settles"); + + // Cancel any remaining intervals + auto *component = id(test_sensor); + App.scheduler.cancel_interval(component, "temp_sensor"); + App.scheduler.cancel_interval(component, "humidity_sensor"); + App.scheduler.cancel_interval(component, "heartbeat"); + + ESP_LOGD("test", "Cancelled any remaining intervals"); + + // The pool should have items from completed timeouts in earlier phases. + // Phase 1 had 3 timeouts that completed and were recycled. + // Phase 3 had 1 timeout that completed and was recycled. + // Phase 4 had 3 defers that completed and were recycled. + // So we should have a decent pool size already from naturally completed items. + + // Now create 8 new timeouts - they should reuse from pool when available + int reuse_test_count = 8; + + for (int i = 0; i < reuse_test_count; i++) { + std::string name = "reuse_test_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for reuse verification", reuse_test_count); + id(create_count) += reuse_test_count; + ESP_LOGI("test", "Phase 5 complete"); + + - id: test_full_pool_reuse + then: + - lambda: |- + ESP_LOGI("test", "Phase 6: Testing pool size limits after Phase 5 items complete"); + + // At this point, all Phase 5 timeouts should have completed and been recycled. + // The pool should be at its maximum size (5). + // Creating 10 new items tests that: + // - First 5 items reuse from the pool + // - Remaining 5 items allocate new (pool empty) + // - Pool doesn't grow beyond MAX_POOL_SIZE of 5 + + auto *component = id(test_sensor); + int full_reuse_count = 10; + + for (int i = 0; i < full_reuse_count; i++) { + std::string name = "full_reuse_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Full reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for full pool reuse verification", full_reuse_count); + id(create_count) += full_reuse_count; + ESP_LOGI("test", "Phase 6 complete"); + + - id: test_same_defer_optimization + then: + - lambda: |- + ESP_LOGI("test", "Phase 7: Testing same-named defer optimization"); + + auto *component = id(test_sensor); + + // Create 10 defers with the same name - should optimize to update callback in-place + // This pattern is common in components like ratgdo that repeatedly defer state updates + for (int i = 0; i < 10; i++) { + App.scheduler.set_timeout(component, "repeated_defer", 0, [i]() { + ESP_LOGD("test", "Repeated defer executed with value: %d", i); + }); + } + + // Only the first should allocate, the rest should update in-place + // We expect only 1 allocation for all 10 operations + id(create_count) += 1; // Only count 1 since others should be optimized + + ESP_LOGD("test", "Created 10 same-named defers (should only allocate once)"); + ESP_LOGI("test", "Phase 7 complete"); + + - id: complete_test + then: + - lambda: |- + ESP_LOGI("test", "Pool recycling test complete - created %d items, cancelled %d, intervals %d", + id(create_count), id(cancel_count), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +# No interval - tests will be triggered from Python via API services diff --git a/tests/integration/test_gpio_expander_cache.py b/tests/integration/test_gpio_expander_cache.py index 9353bb1dd6..e5f0f2818f 100644 --- a/tests/integration/test_gpio_expander_cache.py +++ b/tests/integration/test_gpio_expander_cache.py @@ -30,9 +30,15 @@ async def test_gpio_expander_cache( logs_done = asyncio.Event() - # Patterns to match in logs - digital_read_hw_pattern = re.compile(r"digital_read_hw pin=(\d+)") - digital_read_cache_pattern = re.compile(r"digital_read_cache pin=(\d+)") + # Patterns to match in logs - match any variation of digital_read + read_hw_pattern = re.compile(r"(?:uint16_)?digital_read_hw pin=(\d+)") + read_cache_pattern = re.compile(r"(?:uint16_)?digital_read_cache pin=(\d+)") + + # Keep specific patterns for building the expected order + digital_read_hw_pattern = re.compile(r"^digital_read_hw pin=(\d+)") + digital_read_cache_pattern = re.compile(r"^digital_read_cache pin=(\d+)") + uint16_read_hw_pattern = re.compile(r"^uint16_digital_read_hw pin=(\d+)") + uint16_read_cache_pattern = re.compile(r"^uint16_digital_read_cache pin=(\d+)") # ensure logs are in the expected order log_order = [ @@ -59,6 +65,17 @@ async def test_gpio_expander_cache( (digital_read_cache_pattern, 14), (digital_read_hw_pattern, 14), (digital_read_cache_pattern, 14), + # uint16_t component tests (single bank of 16 pins) + (uint16_read_hw_pattern, 0), # First pin triggers hw read + [ + (uint16_read_cache_pattern, i) for i in range(0, 16) + ], # All 16 pins return via cache + # After cache reset + (uint16_read_hw_pattern, 5), # First read after reset triggers hw + (uint16_read_cache_pattern, 5), + (uint16_read_cache_pattern, 10), # These use cache (same bank) + (uint16_read_cache_pattern, 15), + (uint16_read_cache_pattern, 0), ] # Flatten the log order for easier processing log_order: list[tuple[re.Pattern, int]] = [ @@ -77,17 +94,22 @@ async def test_gpio_expander_cache( clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) - if "digital_read" in clean_line: + # Extract just the log message part (after the log level) + msg = clean_line.split(": ", 1)[-1] if ": " in clean_line else clean_line + + # Check if this line contains a read operation we're tracking + if read_hw_pattern.search(msg) or read_cache_pattern.search(msg): if index >= len(log_order): - print(f"Received unexpected log line: {clean_line}") + print(f"Received unexpected log line: {msg}") logs_done.set() return pattern, expected_pin = log_order[index] - match = pattern.search(clean_line) + match = pattern.search(msg) if not match: - print(f"Log line did not match next expected pattern: {clean_line}") + print(f"Log line did not match next expected pattern: {msg}") + print(f"Expected pattern: {pattern.pattern}") logs_done.set() return @@ -99,9 +121,10 @@ async def test_gpio_expander_cache( index += 1 - elif "DONE" in clean_line: - # Check if we reached the end of the expected log entries - logs_done.set() + elif "DONE_UINT16" in clean_line: + # uint16 component is done, check if we've seen all expected logs + if index == len(log_order): + logs_done.set() # Run with log monitoring async with ( diff --git a/tests/integration/test_scheduler_pool.py b/tests/integration/test_scheduler_pool.py new file mode 100644 index 0000000000..b5f9f12631 --- /dev/null +++ b/tests/integration/test_scheduler_pool.py @@ -0,0 +1,209 @@ +"""Integration test for scheduler memory pool functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_pool( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that the scheduler memory pool is working correctly with realistic usage. + + This test simulates real-world scheduler usage patterns and verifies that: + 1. Items are recycled to the pool when timeouts complete naturally + 2. Items are recycled when intervals/timeouts are cancelled + 3. Items are reused from the pool for new scheduler operations + 4. The pool grows gradually based on actual usage patterns + 5. Pool operations are logged correctly with debug scheduler enabled + """ + # Track log messages to verify pool behavior + log_lines: list[str] = [] + pool_reuse_count = 0 + pool_recycle_count = 0 + pool_full_count = 0 + new_alloc_count = 0 + + # Patterns to match pool operations + reuse_pattern = re.compile(r"Reused item from pool \(pool size now: (\d+)\)") + recycle_pattern = re.compile(r"Recycled item to pool \(pool size now: (\d+)\)") + pool_full_pattern = re.compile(r"Pool full \(size: (\d+)\), deleting item") + new_alloc_pattern = re.compile(r"Allocated new item \(pool empty\)") + + # Futures to track when test phases complete + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + phase_futures = { + 1: loop.create_future(), + 2: loop.create_future(), + 3: loop.create_future(), + 4: loop.create_future(), + 5: loop.create_future(), + 6: loop.create_future(), + 7: loop.create_future(), + } + + def check_output(line: str) -> None: + """Check log output for pool operations and phase completion.""" + nonlocal pool_reuse_count, pool_recycle_count, pool_full_count, new_alloc_count + log_lines.append(line) + + # Track pool operations + if reuse_pattern.search(line): + pool_reuse_count += 1 + + elif recycle_pattern.search(line): + pool_recycle_count += 1 + + elif pool_full_pattern.search(line): + pool_full_count += 1 + + elif new_alloc_pattern.search(line): + new_alloc_count += 1 + + # Track phase completion + for phase_num in range(1, 8): + if ( + f"Phase {phase_num} complete" in line + and phase_num in phase_futures + and not phase_futures[phase_num].done() + ): + phase_futures[phase_num].set_result(True) + + # Check for test completion + if "Pool recycling test complete" in line and not test_complete_future.done(): + test_complete_future.set_result(True) + + # Run the test with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is running + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-pool-test" + + # Get list of services + entities, services = await client.list_entities_services() + service_names = {s.name for s in services} + + # Verify all test services are available + expected_services = { + "run_phase_1", + "run_phase_2", + "run_phase_3", + "run_phase_4", + "run_phase_5", + "run_phase_6", + "run_phase_7", + "run_complete", + } + assert expected_services.issubset(service_names), ( + f"Missing services: {expected_services - service_names}" + ) + + # Get service objects + phase_services = { + num: next(s for s in services if s.name == f"run_phase_{num}") + for num in range(1, 8) + } + complete_service = next(s for s in services if s.name == "run_complete") + + try: + # Phase 1: Component lifecycle + client.execute_service(phase_services[1], {}) + await asyncio.wait_for(phase_futures[1], timeout=1.0) + await asyncio.sleep(0.05) # Let timeouts complete + + # Phase 2: Sensor polling + client.execute_service(phase_services[2], {}) + await asyncio.wait_for(phase_futures[2], timeout=1.0) + await asyncio.sleep(0.1) # Let intervals run a bit + + # Phase 3: Communication patterns + client.execute_service(phase_services[3], {}) + await asyncio.wait_for(phase_futures[3], timeout=1.0) + await asyncio.sleep(0.1) # Let heartbeat run + + # Phase 4: Defer patterns + client.execute_service(phase_services[4], {}) + await asyncio.wait_for(phase_futures[4], timeout=1.0) + await asyncio.sleep(0.2) # Let everything settle and recycle + + # Phase 5: Pool reuse verification + client.execute_service(phase_services[5], {}) + await asyncio.wait_for(phase_futures[5], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 5 timeouts complete and recycle + + # Phase 6: Full pool reuse verification + client.execute_service(phase_services[6], {}) + await asyncio.wait_for(phase_futures[6], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 6 timeouts complete + + # Phase 7: Same-named defer optimization + client.execute_service(phase_services[7], {}) + await asyncio.wait_for(phase_futures[7], timeout=1.0) + await asyncio.sleep(0.05) # Let the single defer execute + + # Complete test + client.execute_service(complete_service, {}) + await asyncio.wait_for(test_complete_future, timeout=0.5) + + except TimeoutError as e: + # Print debug info if test times out + recent_logs = "\n".join(log_lines[-30:]) + phases_completed = [num for num, fut in phase_futures.items() if fut.done()] + pytest.fail( + f"Test timed out waiting for phase/completion. Error: {e}\n" + f" Phases completed: {phases_completed}\n" + f" Pool stats:\n" + f" Reuse count: {pool_reuse_count}\n" + f" Recycle count: {pool_recycle_count}\n" + f" Pool full count: {pool_full_count}\n" + f" New alloc count: {new_alloc_count}\n" + f"Recent logs:\n{recent_logs}" + ) + + # Verify all test phases ran + for phase_num in range(1, 8): + assert phase_futures[phase_num].done(), f"Phase {phase_num} did not complete" + + # Verify pool behavior + assert pool_recycle_count > 0, "Should have recycled items to pool" + + # Check pool metrics + if pool_recycle_count > 0: + max_pool_size = 0 + for line in log_lines: + if match := recycle_pattern.search(line): + size = int(match.group(1)) + max_pool_size = max(max_pool_size, size) + + # Pool can grow up to its maximum of 5 + assert max_pool_size <= 5, f"Pool grew beyond maximum ({max_pool_size})" + + # Log summary for debugging + print("\nScheduler Pool Test Summary (Python Orchestrated):") + print(f" Items recycled to pool: {pool_recycle_count}") + print(f" Items reused from pool: {pool_reuse_count}") + print(f" Pool full events: {pool_full_count}") + print(f" New allocations: {new_alloc_count}") + print(" All phases completed successfully") + + # Verify reuse happened + if pool_reuse_count == 0 and pool_recycle_count > 3: + pytest.fail("Pool had items recycled but none were reused") + + # Success - pool is working + assert pool_recycle_count > 0 or new_alloc_count < 15, ( + "Pool should either recycle items or limit new allocations" + ) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index b353d1aa99..9f51206ff9 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,8 +1,14 @@ +import logging +import socket +from unittest.mock import patch + +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr from hypothesis import given from hypothesis.strategies import ip_addresses import pytest from esphome import helpers +from esphome.core import EsphomeError @pytest.mark.parametrize( @@ -277,3 +283,314 @@ def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None: actual = helpers.sort_ip_addresses(text) assert actual == expected + + +# DNS resolution tests +def test_is_ip_address_ipv4() -> None: + """Test is_ip_address with IPv4 addresses.""" + assert helpers.is_ip_address("192.168.1.1") is True + assert helpers.is_ip_address("127.0.0.1") is True + assert helpers.is_ip_address("255.255.255.255") is True + assert helpers.is_ip_address("0.0.0.0") is True + + +def test_is_ip_address_ipv6() -> None: + """Test is_ip_address with IPv6 addresses.""" + assert helpers.is_ip_address("::1") is True + assert helpers.is_ip_address("2001:db8::1") is True + assert helpers.is_ip_address("fe80::1") is True + assert helpers.is_ip_address("::") is True + + +def test_is_ip_address_invalid() -> None: + """Test is_ip_address with non-IP strings.""" + assert helpers.is_ip_address("hostname") is False + assert helpers.is_ip_address("hostname.local") is False + assert helpers.is_ip_address("256.256.256.256") is False + assert helpers.is_ip_address("192.168.1") is False + assert helpers.is_ip_address("") is False + + +def test_resolve_ip_address_single_ipv4() -> None: + """Test resolving a single IPv4 address (fast path).""" + result = helpers.resolve_ip_address("192.168.1.100", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + assert result[0][4] == ("192.168.1.100", 6053) # sockaddr + + +def test_resolve_ip_address_single_ipv6() -> None: + """Test resolving a single IPv6 address (fast path).""" + result = helpers.resolve_ip_address("::1", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + # IPv6 sockaddr has 4 elements + assert len(result[0][4]) == 4 + assert result[0][4][0] == "::1" # address + assert result[0][4][1] == 6053 # port + + +def test_resolve_ip_address_list_of_ips() -> None: + """Test resolving a list of IP addresses (fast path).""" + ips = ["192.168.1.100", "10.0.0.1", "::1"] + result = helpers.resolve_ip_address(ips, 6053) + + # Should return results sorted by preference (IPv6 first, then IPv4) + assert len(result) >= 2 # At least IPv4 addresses should work + + # Check that results are properly formatted + for addr_info in result: + assert addr_info[0] in (socket.AF_INET, socket.AF_INET6) + assert addr_info[1] in ( + 0, + socket.SOCK_STREAM, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[2] in ( + 0, + socket.IPPROTO_TCP, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[3] == "" + + +def test_resolve_ip_address_with_getaddrinfo_failure(caplog) -> None: + """Test that getaddrinfo OSError is handled gracefully in fast path.""" + with ( + caplog.at_level(logging.DEBUG), + patch("socket.getaddrinfo") as mock_getaddrinfo, + ): + # First IP succeeds + mock_getaddrinfo.side_effect = [ + [ + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.100", 6053), + ) + ], + OSError("Failed to resolve"), # Second IP fails + ] + + # Should continue despite one failure + result = helpers.resolve_ip_address(["192.168.1.100", "192.168.1.101"], 6053) + + # Should have result from first IP only + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.100" + + # Verify both IPs were attempted + assert mock_getaddrinfo.call_count == 2 + mock_getaddrinfo.assert_any_call( + "192.168.1.100", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + mock_getaddrinfo.assert_any_call( + "192.168.1.101", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + + # Verify the debug log was called for the failed IP + assert "Failed to parse IP address '192.168.1.101'" in caplog.text + + +def test_resolve_ip_address_hostname() -> None: + """Test resolving a hostname (async resolver path).""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET + assert result[0][4] == ("192.168.1.100", 6053) + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list() -> None: + """Test resolving a mix of IPs and hostnames.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.200", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_url() -> None: + """Test extracting hostname from URL.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("http://test.local", 6053) + + assert len(result) == 1 + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_ipv6_conversion() -> None: + """Test proper IPv6 address info conversion.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=1, scope_id=2), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 + assert result[0][4] == ("2001:db8::1", 6053, 1, 2) + + +def test_resolve_ip_address_error_handling() -> None: + """Test error handling from AsyncResolver.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError("Resolution failed") + + with pytest.raises(EsphomeError, match="Resolution failed"): + helpers.resolve_ip_address("test.local", 6053) + + +def test_addr_preference_ipv4() -> None: + """Test address preference for IPv4.""" + addr_info = ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.1", 6053), + ) + assert helpers.addr_preference_(addr_info) == 2 + + +def test_addr_preference_ipv6() -> None: + """Test address preference for regular IPv6.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("2001:db8::1", 6053, 0, 0), + ) + assert helpers.addr_preference_(addr_info) == 1 + + +def test_addr_preference_ipv6_link_local_no_scope() -> None: + """Test address preference for link-local IPv6 without scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 0), # link-local with scope_id=0 + ) + assert helpers.addr_preference_(addr_info) == 3 + + +def test_addr_preference_ipv6_link_local_with_scope() -> None: + """Test address preference for link-local IPv6 with scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 2), # link-local with scope_id=2 + ) + assert helpers.addr_preference_(addr_info) == 1 # Has scope, so it's usable + + +def test_resolve_ip_address_sorting() -> None: + """Test that results are sorted by preference.""" + # Create multiple address infos with different preferences + mock_addr_infos = [ + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="fe80::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 3 (link-local no scope) + ), + AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr( + address="192.168.1.100", port=6053 + ), # Preference 2 (IPv4) + ), + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="2001:db8::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 1 (IPv6) + ), + ] + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = mock_addr_infos + + result = helpers.resolve_ip_address("test.local", 6053) + + # Should be sorted: IPv6 first, then IPv4, then link-local without scope + assert result[0][4][0] == "2001:db8::1" # IPv6 (preference 1) + assert result[1][4][0] == "192.168.1.100" # IPv4 (preference 2) + assert result[2][4][0] == "fe80::1" # Link-local no scope (preference 3) diff --git a/tests/unit_tests/test_resolver.py b/tests/unit_tests/test_resolver.py new file mode 100644 index 0000000000..b4cca05d9f --- /dev/null +++ b/tests/unit_tests/test_resolver.py @@ -0,0 +1,169 @@ +"""Tests for the DNS resolver module.""" + +from __future__ import annotations + +import re +import socket +from unittest.mock import patch + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr +import pytest + +from esphome.core import EsphomeError +from esphome.resolver import RESOLVE_TIMEOUT, AsyncResolver + + +@pytest.fixture +def mock_addr_info_ipv4() -> AddrInfo: + """Create a mock IPv4 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + +@pytest.fixture +def mock_addr_info_ipv6() -> AddrInfo: + """Create a mock IPv6 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=0, scope_id=0), + ) + + +def test_async_resolver_successful_resolution(mock_addr_info_ipv4: AddrInfo) -> None: + """Test successful DNS resolution.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["test.local"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["test.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_multiple_hosts( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving multiple hosts.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test1.local", "test2.local"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test1.local", "test2.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_resolve_api_error() -> None: + """Test handling of ResolveAPIError.""" + error_msg = "Failed to resolve" + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises( + EsphomeError, match=re.escape(f"Error resolving IP address: {error_msg}") + ): + resolver.resolve() + + +def test_async_resolver_timeout_error() -> None: + """Test handling of ResolveTimeoutAPIError.""" + error_msg = "Resolution timed out" + + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveTimeoutAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Match either "Timeout" or "Error" since ResolveTimeoutAPIError is a subclass of ResolveAPIError + # and depending on import order/test execution context, it might be caught as either + with pytest.raises( + EsphomeError, + match=f"(Timeout|Error) resolving IP address: {re.escape(error_msg)}", + ): + resolver.resolve() + + +def test_async_resolver_generic_exception() -> None: + """Test handling of generic exceptions.""" + error = RuntimeError("Unexpected error") + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=error, + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises(RuntimeError, match="Unexpected error"): + resolver.resolve() + + +def test_async_resolver_thread_timeout() -> None: + """Test timeout when thread doesn't complete in time.""" + # Mock the start method to prevent actual thread execution + with ( + patch.object(AsyncResolver, "start"), + patch("esphome.resolver.hr.async_resolve_host"), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Override event.wait to simulate timeout (return False = timeout occurred) + with ( + patch.object(resolver.event, "wait", return_value=False), + pytest.raises( + EsphomeError, match=re.escape("Timeout resolving IP address") + ), + ): + resolver.resolve() + + # Verify thread start was called + resolver.start.assert_called_once() + + +def test_async_resolver_ip_addresses(mock_addr_info_ipv4: AddrInfo) -> None: + """Test resolving IP addresses.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["192.168.1.100"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["192.168.1.100"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_mixed_addresses( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving mix of hostnames and IP addresses.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test.local", "192.168.1.100", "::1"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test.local", "192.168.1.100", "::1"], 6053, timeout=RESOLVE_TIMEOUT + )